diff options
183 files changed, 2422 insertions, 1269 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 301092317fe..421ab545d9a 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.46.0 +0.47.0 @@ -398,7 +398,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.41.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.42.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index f5712da7508..afb0b6b471d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -273,7 +273,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.41.0) + gitaly-proto (0.42.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -324,7 +324,9 @@ GEM mime-types (~> 3.0) representable (~> 3.0) retriable (>= 2.0, < 4.0) - google-protobuf (3.4.0.2) + google-protobuf (3.4.1.1) + googleapis-common-protos-types (1.0.0) + google-protobuf (~> 3.0) googleauth (0.5.3) faraday (~> 0.12) jwt (~> 1.4) @@ -351,8 +353,9 @@ GEM rake grape_logging (1.7.0) grape - grpc (1.6.0) + grpc (1.6.6) google-protobuf (~> 3.1) + googleapis-common-protos-types (~> 1.0.0) googleauth (~> 0.5.1) haml (4.0.7) tilt @@ -1027,7 +1030,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.41.0) + gitaly-proto (~> 0.42.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 38d1effc77c..242b3e2b990 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -15,6 +15,7 @@ const Api = { issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', usersPath: '/api/:version/users.json', commitPath: '/api/:version/projects/:id/repository/commits', + branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath) @@ -123,6 +124,19 @@ const Api = { }); }, + branchSingle(id, branch) { + const url = Api.buildUrl(Api.branchSinglePath) + .replace(':id', id) + .replace(':branch', branch); + + return this.wrapAjaxCall({ + url, + type: 'GET', + contentType: 'application/json; charset=utf-8', + dataType: 'json', + }); + }, + // Return text for a specific license licenseText(key, data, callback) { const url = Api.buildUrl(Api.licensePath) diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 38eea38f949..97e80afa3f8 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -7,7 +7,7 @@ class BoardService { this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, { issues: { method: 'GET', - url: `${gon.relative_url_root}/boards/${boardId}/issues.json`, + url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`, } }); this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, { @@ -16,7 +16,7 @@ class BoardService { url: `${listsEndpoint}/generate.json` } }); - this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {}); + this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {}); this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, { bulkUpdate: { method: 'POST', diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index f73e489e7b2..ff88083a4b4 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -1,33 +1,28 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */ - -$(function() { - var previewPath; - $('input#broadcast_message_color').on('input', function() { - var previewColor; - previewColor = $(this).val(); - return $('div.broadcast-message-preview').css('background-color', previewColor); +export default function initBroadcastMessagesForm() { + $('input#broadcast_message_color').on('input', function onMessageColorInput() { + const previewColor = $(this).val(); + $('div.broadcast-message-preview').css('background-color', previewColor); }); - $('input#broadcast_message_font').on('input', function() { - var previewColor; - previewColor = $(this).val(); - return $('div.broadcast-message-preview').css('color', previewColor); + + $('input#broadcast_message_font').on('input', function onMessageFontInput() { + const previewColor = $(this).val(); + $('div.broadcast-message-preview').css('color', previewColor); }); - previewPath = $('textarea#broadcast_message_message').data('preview-path'); - return $('textarea#broadcast_message_message').on('input', function() { - var message; - message = $(this).val(); + + const previewPath = $('textarea#broadcast_message_message').data('preview-path'); + + $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() { + const message = $(this).val(); if (message === '') { - return $('.js-broadcast-message-preview').text("Your message here"); + $('.js-broadcast-message-preview').text('Your message here'); } else { - return $.ajax({ + $.ajax({ url: previewPath, - type: "POST", + type: 'POST', data: { - broadcast_message: { - message: message - } - } + broadcast_message: { message }, + }, }); } - }); -}); + }, 250)); +} diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 0a653d7fefc..0a645e5842d 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,8 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* global ProjectSelect */ -/* global ShortcutsNavigation */ /* global IssuableIndex */ -/* global ShortcutsIssuable */ /* global Milestone */ /* global IssuableForm */ /* global LabelsSelect */ @@ -31,10 +29,7 @@ import CILintEditor from './ci_lint_editor'; /* global ProjectImport */ import Labels from './labels'; import LabelManager from './label_manager'; -/* global Shortcuts */ -/* global ShortcutsFindFile */ /* global Sidebar */ -/* global ShortcutsWiki */ import CommitsList from './commits'; import Issue from './issue'; @@ -70,6 +65,7 @@ import initSettingsPanels from './settings_panels'; import initExperimentalFlags from './experimental_flags'; import OAuthRememberMe from './oauth_remember_me'; import PerformanceBar from './performance_bar'; +import initBroadcastMessagesForm from './broadcast_message'; import initNotes from './init_notes'; import initLegacyFilters from './init_legacy_filters'; import initIssuableSidebar from './init_issuable_sidebar'; @@ -82,7 +78,13 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; import AjaxLoadingSpinner from './ajax_loading_spinner'; import GlFieldErrors from './gl_field_errors'; import GLForm from './gl_form'; +import Shortcuts from './shortcuts'; +import ShortcutsNavigation from './shortcuts_navigation'; +import ShortcutsFindFile from './shortcuts_find_file'; +import ShortcutsIssuable from './shortcuts_issuable'; import U2FAuthenticate from './u2f/authenticate'; +import Members from './members'; +import memberExpirationDate from './member_expiration_date'; (function() { var Dispatcher; @@ -166,9 +168,6 @@ import U2FAuthenticate from './u2f/authenticate'; const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } - if (page === 'projects:merge_requests:index') { - new UserCallout({ setCalloutPerProject: true }); - } const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; IssuableIndex.init(pagePrefix); @@ -350,7 +349,10 @@ import U2FAuthenticate from './u2f/authenticate'; case 'projects:show': shortcut_handler = new ShortcutsNavigation(); new NotificationsForm(); - new UserCallout({ setCalloutPerProject: true }); + new UserCallout({ + setCalloutPerProject: true, + className: 'js-autodevops-banner', + }); if ($('#tree-slider').length) new TreeView(); if ($('.blob-viewer').length) new BlobViewer(); @@ -370,9 +372,6 @@ import U2FAuthenticate from './u2f/authenticate'; case 'projects:pipelines:new': new NewBranchForm($('.js-new-pipeline-form')); break; - case 'projects:pipelines:index': - new UserCallout({ setCalloutPerProject: true }); - break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': @@ -399,15 +398,15 @@ import U2FAuthenticate from './u2f/authenticate'; new ProjectsList(); break; case 'groups:group_members:index': - new gl.MemberExpirationDate(); - new gl.Members(); + memberExpirationDate(); + new Members(); new UsersSelect(); break; case 'projects:project_members:index': - new gl.MemberExpirationDate('.js-access-expiration-date-groups'); + memberExpirationDate('.js-access-expiration-date-groups'); new GroupsSelect(); - new gl.MemberExpirationDate(); - new gl.Members(); + memberExpirationDate(); + new Members(); new UsersSelect(); break; case 'groups:new': @@ -430,7 +429,6 @@ import U2FAuthenticate from './u2f/authenticate'; new TreeView(); new BlobViewer(); new NewCommitForm($('.js-create-dir-form')); - new UserCallout({ setCalloutPerProject: true }); $('#tree-slider').waitForImages(function() { ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); @@ -553,6 +551,9 @@ import U2FAuthenticate from './u2f/authenticate'; case 'admin': new Admin(); switch (path[1]) { + case 'broadcast_messages': + initBroadcastMessagesForm(); + break; case 'cohorts': new UsagePing(); break; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index bc5cd818e1c..67261c1c9b4 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -40,6 +40,10 @@ const createFlashEl = (message, type, isInContentWrapper = false) => ` </div> `; +const removeFlashClickListener = (flashEl, fadeTransition) => { + flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); +}; + /* * Flash banner supports different types of Flash configurations * along with ability to provide actionConfig which can be used to show @@ -70,7 +74,7 @@ const createFlash = function createFlash( flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper); const flashEl = flashContainer.querySelector(`.flash-${type}`); - flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); + removeFlashClickListener(flashEl, fadeTransition); if (actionConfig) { flashEl.innerHTML += createAction(actionConfig); @@ -90,5 +94,6 @@ export { createFlashEl, createAction, hideFlash, + removeFlashClickListener, }; window.Flash = createFlash; diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 3f6f40d47ba..6d671845f8e 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -43,16 +43,6 @@ type: 'link', }); } - - if (this.job.retry_path) { - actions.push({ - label: 'Retry', - path: this.job.retry_path, - cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block', - type: 'ujs-link', - }); - } - return actions; }, }, diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 78c7a094127..1aa63216baf 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) { return hashIndex === -1 ? null : url.substring(hashIndex + 1); }; -w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); +w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href); // eslint-disable-next-line import/prefer-default-export export function visitUrl(url, external = false) { @@ -96,7 +96,7 @@ export function visitUrl(url, external = false) { otherWindow.opener = null; otherWindow.location = url; } else { - document.location.href = url; + window.location.href = url; } } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 8d7608ce0f4..2fc47d5963b 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -21,15 +21,6 @@ window._ = _; window.Dropzone = Dropzone; window.Sortable = Sortable; -// shortcuts -import './shortcuts'; -import './shortcuts_blob'; -import './shortcuts_dashboard_navigation'; -import './shortcuts_navigation'; -import './shortcuts_find_file'; -import './shortcuts_issuable'; -import './shortcuts_network'; - // templates import './templates/issuable_template_selector'; import './templates/issuable_template_selectors'; @@ -53,7 +44,6 @@ import './aside'; import './autosave'; import loadAwardsHandler from './awards_handler'; import bp from './breakpoints'; -import './broadcast_message'; import './commits'; import './compare'; import './compare_autocomplete'; @@ -64,7 +54,7 @@ import './diff'; import './dropzone_input'; import './due_date_select'; import './files_comment_button'; -import Flash from './flash'; +import Flash, { removeFlashClickListener } from './flash'; import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; @@ -84,8 +74,6 @@ import './layout_nav'; import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; -import './member_expiration_date'; -import './members'; import './merge_request'; import './merge_request_tabs'; import './milestone'; @@ -339,4 +327,10 @@ $(function () { event.preventDefault(); gl.utils.visitUrl(`${action}${$(this).serialize()}`); }); + + const flashContainer = document.querySelector('.flash-container'); + + if (flashContainer && flashContainer.children.length) { + removeFlashClickListener(flashContainer.children[0]); + } }); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index cc9016e74da..26b24fdafda 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -2,54 +2,51 @@ import Pikaday from 'pikaday'; -(() => { - // Add datepickers to all `js-access-expiration-date` elements. If those elements are - // children of an element with the `clearable-input` class, and have a sibling - // `js-clear-input` element, then show that element when there is a value in the - // datepicker, and make clicking on that element clear the field. - // - window.gl = window.gl || {}; - gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => { - function toggleClearInput() { - $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); - } - const inputs = $(selector); - - inputs.each((i, el) => { - const $input = $(el); - - const calendar = new Pikaday({ - field: $input.get(0), - theme: 'gitlab-theme animate-picker', - format: 'yyyy-mm-dd', - minDate: new Date(), - container: $input.parent().get(0), - onSelect(dateText) { - $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); - - $input.trigger('change'); - - toggleClearInput.call($input); - }, - }); - - calendar.setDate(new Date($input.val())); - $input.data('pikaday', calendar); +// Add datepickers to all `js-access-expiration-date` elements. If those elements are +// children of an element with the `clearable-input` class, and have a sibling +// `js-clear-input` element, then show that element when there is a value in the +// datepicker, and make clicking on that element clear the field. +// +export default function memberExpirationDate(selector = '.js-access-expiration-date') { + function toggleClearInput() { + $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); + } + const inputs = $(selector); + + inputs.each((i, el) => { + const $input = $(el); + + const calendar = new Pikaday({ + field: $input.get(0), + theme: 'gitlab-theme animate-picker', + format: 'yyyy-mm-dd', + minDate: new Date(), + container: $input.parent().get(0), + onSelect(dateText) { + $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); + + $input.trigger('change'); + + toggleClearInput.call($input); + }, }); - inputs.next('.js-clear-input').on('click', function clicked(event) { - event.preventDefault(); + calendar.setDate(new Date($input.val())); + $input.data('pikaday', calendar); + }); - const input = $(this).closest('.clearable-input').find(selector); - const calendar = input.data('pikaday'); + inputs.next('.js-clear-input').on('click', function clicked(event) { + event.preventDefault(); - calendar.setDate(null); - input.trigger('change'); - toggleClearInput.call(input); - }); + const input = $(this).closest('.clearable-input').find(selector); + const calendar = input.data('pikaday'); + + calendar.setDate(null); + input.trigger('change'); + toggleClearInput.call(input); + }); - inputs.on('blur', toggleClearInput); + inputs.on('blur', toggleClearInput); - inputs.each(toggleClearInput); - }; -}).call(window); + inputs.each(toggleClearInput); +} diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 8291b8c4a70..6264750a4fb 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -1,81 +1,74 @@ -/* eslint-disable class-methods-use-this */ -(() => { - window.gl = window.gl || {}; - - class Members { - constructor() { - this.addListeners(); - this.initGLDropdown(); - } +export default class Members { + constructor() { + this.addListeners(); + this.initGLDropdown(); + } - addListeners() { - $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); - $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); - $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); - gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); - } + addListeners() { + $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); + $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); + $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); + gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); + } - initGLDropdown() { - $('.js-member-permissions-dropdown').each((i, btn) => { - const $btn = $(btn); + initGLDropdown() { + $('.js-member-permissions-dropdown').each((i, btn) => { + const $btn = $(btn); - $btn.glDropdown({ - selectable: true, - isSelectable(selected, $el) { - return !$el.hasClass('is-active'); - }, - fieldName: $btn.data('field-name'), - id(selected, $el) { - return $el.data('id'); - }, - toggleLabel(selected, $el) { - return $el.text(); - }, - clicked: (options) => { - this.formSubmit(null, options.$el); - }, - }); + $btn.glDropdown({ + selectable: true, + isSelectable(selected, $el) { + return !$el.hasClass('is-active'); + }, + fieldName: $btn.data('field-name'), + id(selected, $el) { + return $el.data('id'); + }, + toggleLabel(selected, $el) { + return $el.text(); + }, + clicked: (options) => { + this.formSubmit(null, options.$el); + }, }); - } - - removeRow(e) { - const $target = $(e.target); + }); + } + // eslint-disable-next-line class-methods-use-this + removeRow(e) { + const $target = $(e.target); - if ($target.hasClass('btn-remove')) { - $target.closest('.member') - .fadeOut(function fadeOutMemberRow() { - $(this).remove(); - }); - } + if ($target.hasClass('btn-remove')) { + $target.closest('.member') + .fadeOut(function fadeOutMemberRow() { + $(this).remove(); + }); } + } - formSubmit(e, $el = null) { - const $this = e ? $(e.currentTarget) : $el; - const { $toggle, $dateInput } = this.getMemberListItems($this); - - $this.closest('form').trigger('submit.rails'); - - $toggle.disable(); - $dateInput.disable(); - } + formSubmit(e, $el = null) { + const $this = e ? $(e.currentTarget) : $el; + const { $toggle, $dateInput } = this.getMemberListItems($this); - formSuccess(e) { - const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); + $this.closest('form').trigger('submit.rails'); - $toggle.enable(); - $dateInput.enable(); - } + $toggle.disable(); + $dateInput.disable(); + } - getMemberListItems($el) { - const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); + formSuccess(e) { + const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member')); - return { - $memberListItem, - $toggle: $memberListItem.find('.dropdown-menu-toggle'), - $dateInput: $memberListItem.find('.js-access-expiration-date'), - }; - } + $toggle.enable(); + $dateInput.enable(); } + // eslint-disable-next-line class-methods-use-this + getMemberListItems($el) { + const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); - gl.Members = Members; -})(); + return { + $memberListItem, + $toggle: $memberListItem.find('.dropdown-menu-toggle'), + $dateInput: $memberListItem.find('.js-access-expiration-date'), + }; + } +} diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 951d5e559b4..e7d5325a509 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -146,7 +146,9 @@ import _ from 'underscore'; clicked: function(options) { const { $el, e } = options; let selected = options.selectedObj; + var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; + if (!selected) return; page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 8aae2ad201c..129f1724cb8 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ -/* global ShortcutsNetwork */ +import ShortcutsNetwork from '../shortcuts_network'; import Network from './network'; $(function() { diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 6d8cc964eb2..c0dc4c8cd8b 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -3,11 +3,17 @@ import Flash from '../../flash'; import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; import Service from '../services/repo_service'; +import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import { visitUrl } from '../../lib/utils/url_utility'; export default { + mixins: [RepoMixin], + data: () => Store, - mixins: [RepoMixin], + components: { + PopupDialog, + }, computed: { showCommitable() { @@ -28,7 +34,16 @@ export default { }, methods: { - makeCommit() { + commitToNewBranch(status) { + if (status) { + this.showNewBranchDialog = false; + this.tryCommit(null, true, true); + } else { + // reset the state + } + }, + + makeCommit(newBranch) { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const commitMessage = this.commitMessage; const actions = this.changedFiles.map(f => ({ @@ -36,19 +51,63 @@ export default { file_path: f.path, content: f.newContent, })); + const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch; const payload = { - branch: Store.currentBranch, + branch, commit_message: commitMessage, actions, }; - Store.submitCommitsLoading = true; + if (newBranch) { + payload.start_branch = this.currentBranch; + } + this.submitCommitsLoading = true; Service.commitFiles(payload) - .then(this.resetCommitState) - .catch(() => Flash('An error occurred while committing your changes')); + .then(() => { + this.resetCommitState(); + if (this.startNewMR) { + this.redirectToNewMr(branch); + } else { + this.redirectToBranch(branch); + } + }) + .catch(() => { + Flash('An error occurred while committing your changes'); + }); + }, + + tryCommit(e, skipBranchCheck = false, newBranch = false) { + if (skipBranchCheck) { + this.makeCommit(newBranch); + } else { + Store.setBranchHash() + .then(() => { + if (Store.branchChanged) { + Store.showNewBranchDialog = true; + return; + } + this.makeCommit(newBranch); + }) + .catch(() => { + Flash('An error occurred while committing your changes'); + }); + } + }, + + redirectToNewMr(branch) { + visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch)); + }, + + redirectToBranch(branch) { + visitUrl(this.customBranchURL.replace('{{branch}}', branch)); }, resetCommitState() { this.submitCommitsLoading = false; + this.openedFiles = this.openedFiles.map((file) => { + const f = file; + f.changed = false; + return f; + }); this.changedFiles = []; this.commitMessage = ''; this.editMode = false; @@ -62,9 +121,17 @@ export default { <div v-if="showCommitable" id="commit-area"> + <popup-dialog + v-if="showNewBranchDialog" + :primary-button-label="__('Create new branch')" + kind="primary" + :title="__('Branch has changed')" + :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" + @submit="commitToNewBranch" + /> <form class="form-horizontal" - @submit.prevent="makeCommit"> + @submit.prevent="tryCommit"> <fieldset> <div class="form-group"> <label class="col-md-4 control-label staged-files"> @@ -117,7 +184,7 @@ export default { class="btn btn-success"> <i v-if="submitCommitsLoading" - class="fa fa-spinner fa-spin" + class="js-commit-loading-icon fa fa-spinner fa-spin" aria-hidden="true" aria-label="loading"> </i> @@ -126,6 +193,14 @@ export default { </span> </button> </div> + <div class="col-md-offset-4 col-md-6"> + <div class="checkbox"> + <label> + <input type="checkbox" v-model="startNewMR"> + <span>Start a <strong>new merge request</strong> with these changes</span> + </label> + </div> + </div> </fieldset> </form> </div> diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 02d9c775046..2c597a3cd65 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -22,7 +22,8 @@ const RepoEditor = { const monacoInstance = Helper.monaco.editor.create(this.$el, { model: null, readOnly: false, - contextmenu: false, + contextmenu: true, + scrollBeyondLastLine: false, }); Helper.monacoInstance = monacoInstance; diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 7d0123e3d3a..1a09f411b22 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -31,8 +31,11 @@ function setInitialStore(data) { Store.projectUrl = data.projectUrl; Store.canCommit = data.canCommit; Store.onTopOfBranch = data.onTopOfBranch; + Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl); + Store.customBranchURL = decodeURIComponent(data.blobUrl); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.checkIsCommitable(); + Store.setBranchHash(); } function initRepo(el) { diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index 830685f7e6e..d68d71a4629 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -64,6 +64,10 @@ const RepoService = { return urlArray.join('/'); }, + getBranch() { + return Api.branchSingle(Store.projectId, Store.currentBranch); + }, + commitFiles(payload) { return Api.commitMultiple(Store.projectId, payload) .then(this.commitFlash); diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index c633f538c1b..f8d29af7ffe 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -23,6 +23,7 @@ const RepoStore = { title: '', status: false, }, + showNewBranchDialog: false, activeFile: Helper.getDefaultActiveFile(), activeFileIndex: 0, activeLine: -1, @@ -31,6 +32,12 @@ const RepoStore = { isCommitable: false, binary: false, currentBranch: '', + startNewMR: false, + currentHash: '', + currentShortHash: '', + customBranchURL: '', + newMrTemplateUrl: '', + branchChanged: false, commitMessage: '', binaryTypes: { png: false, @@ -49,6 +56,17 @@ const RepoStore = { }); }, + setBranchHash() { + return Service.getBranch() + .then((data) => { + if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) { + RepoStore.branchChanged = true; + } + RepoStore.currentHash = data.commit.id; + RepoStore.currentShortHash = data.commit.short_id; + }); + }, + // mutations checkIsCommitable() { RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index f63b99ba1c5..ebe7a99ffae 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,128 +1,116 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ -/* global Mousetrap */ import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; - import findAndFollowLink from './shortcuts_dashboard_navigation'; -(function() { - this.Shortcuts = (function() { - function Shortcuts(skipResetBindings) { - this.onToggleHelp = this.onToggleHelp.bind(this); - this.enabledHelp = []; - if (!skipResetBindings) { - Mousetrap.reset(); - } - Mousetrap.bind('?', this.onToggleHelp); - Mousetrap.bind('s', Shortcuts.focusSearch); - Mousetrap.bind('f', (e => this.focusFilter(e))); - Mousetrap.bind('p b', this.onTogglePerfBar); - - const findFileURL = document.body.dataset.findFile; - - Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); - Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); - Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); - Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests')); - Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects')); - Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups')); - Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones')); - Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets')); - - Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); - if (typeof findFileURL !== "undefined" && findFileURL !== null) { - Mousetrap.bind('t', function() { - return gl.utils.visitUrl(findFileURL); - }); - } +const defaultStopCallback = Mousetrap.stopCallback; +Mousetrap.stopCallback = (e, element, combo) => { + if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { + return false; + } + + return defaultStopCallback(e, element, combo); +}; + +export default class Shortcuts { + constructor(skipResetBindings) { + this.onToggleHelp = this.onToggleHelp.bind(this); + this.enabledHelp = []; + if (!skipResetBindings) { + Mousetrap.reset(); } + Mousetrap.bind('?', this.onToggleHelp); + Mousetrap.bind('s', Shortcuts.focusSearch); + Mousetrap.bind('f', this.focusFilter.bind(this)); + Mousetrap.bind('p b', Shortcuts.onTogglePerfBar); - Shortcuts.prototype.onToggleHelp = function(e) { - e.preventDefault(); - return Shortcuts.toggleHelp(this.enabledHelp); - }; + const findFileURL = document.body.dataset.findFile; + + Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos')); + Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity')); + Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues')); + Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests')); + Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects')); + Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups')); + Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones')); + Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets')); + + Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview); - Shortcuts.prototype.onTogglePerfBar = function(e) { + if (typeof findFileURL !== 'undefined' && findFileURL !== null) { + Mousetrap.bind('t', () => { + gl.utils.visitUrl(findFileURL); + }); + } + + $(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) { + $(this).remove(); + $('.hidden-shortcut').show(); e.preventDefault(); - const performanceBarCookieName = 'perf_bar_enabled'; - if (Cookies.get(performanceBarCookieName) === 'true') { - Cookies.remove(performanceBarCookieName, { path: '/' }); - } else { - Cookies.set(performanceBarCookieName, 'true', { path: '/' }); - } - gl.utils.refreshCurrentPage(); - }; - - Shortcuts.prototype.toggleMarkdownPreview = function(e) { - // Check if short-cut was triggered while in Write Mode - const $target = $(e.target); - const $form = $target.closest('form'); - - if ($target.hasClass('js-note-text')) { - $('.js-md-preview-button', $form).focus(); - } - return $(document).triggerHandler('markdown-preview:toggle', [e]); - }; - - Shortcuts.toggleHelp = function(location) { - var $modal; - $modal = $('#modal-shortcuts'); - if ($modal.length) { - $modal.modal('toggle'); - return; - } - return $.ajax({ - url: gon.shortcuts_path, - dataType: 'script', - success: function(e) { - var i, l, len, results; - if (location && location.length > 0) { - results = []; - for (i = 0, len = location.length; i < len; i += 1) { - l = location[i]; - results.push($(l).show()); - } - return results; - } else { - $('.hidden-shortcut').show(); - return $('.js-more-help-button').remove(); + }); + } + + onToggleHelp(e) { + e.preventDefault(); + Shortcuts.toggleHelp(this.enabledHelp); + } + + static onTogglePerfBar(e) { + e.preventDefault(); + const performanceBarCookieName = 'perf_bar_enabled'; + if (Cookies.get(performanceBarCookieName) === 'true') { + Cookies.remove(performanceBarCookieName, { path: '/' }); + } else { + Cookies.set(performanceBarCookieName, 'true', { path: '/' }); + } + gl.utils.refreshCurrentPage(); + } + + static toggleMarkdownPreview(e) { + // Check if short-cut was triggered while in Write Mode + const $target = $(e.target); + const $form = $target.closest('form'); + + if ($target.hasClass('js-note-text')) { + $('.js-md-preview-button', $form).focus(); + } + $(document).triggerHandler('markdown-preview:toggle', [e]); + } + + static toggleHelp(location) { + const $modal = $('#modal-shortcuts'); + + if ($modal.length) { + $modal.modal('toggle'); + } + + $.ajax({ + url: gon.shortcuts_path, + dataType: 'script', + success() { + if (location && location.length > 0) { + const results = []; + for (let i = 0, len = location.length; i < len; i += 1) { + results.push($(location[i]).show()); } + return results; } - }); - }; - - Shortcuts.prototype.focusFilter = function(e) { - if (this.filterInput == null) { - this.filterInput = $('input[type=search]', '.nav-controls'); - } - this.filterInput.focus(); - return e.preventDefault(); - }; - - Shortcuts.focusSearch = function(e) { - $('#search').focus(); - return e.preventDefault(); - }; - - return Shortcuts; - })(); - - $(document).on('click.more_help', '.js-more-help-button', function(e) { - $(this).remove(); - $('.hidden-shortcut').show(); - return e.preventDefault(); - }); - - Mousetrap.stopCallback = (function() { - var defaultStopCallback; - defaultStopCallback = Mousetrap.stopCallback; - return function(e, element, combo) { - // allowed shortcuts if textarea, input, contenteditable are focused - if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { - return false; - } else { - return defaultStopCallback.apply(this, arguments); - } - }; - })(); -}).call(window); + + $('.hidden-shortcut').show(); + return $('.js-more-help-button').remove(); + }, + }); + } + + focusFilter(e) { + if (!this.filterInput) { + this.filterInput = $('input[type=search]', '.nav-controls'); + } + this.filterInput.focus(); + e.preventDefault(); + } + + static focusSearch(e) { + $('#search').focus(); + e.preventDefault(); + } +} diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index ccbf7c59165..fbc57bb4304 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,7 +1,6 @@ /* global Mousetrap */ -/* global Shortcuts */ -import './shortcuts'; +import Shortcuts from './shortcuts'; const defaults = { skipResetBindings: false, diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index b18b6139b35..81286c0010c 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,38 +1,30 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */ /* global Mousetrap */ -/* global ShortcutsNavigation */ -import './shortcuts_navigation'; +import ShortcutsNavigation from './shortcuts_navigation'; -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; +export default class ShortcutsFindFile extends ShortcutsNavigation { + constructor(projectFindFile) { + super(); - this.ShortcutsFindFile = (function(superClass) { - extend(ShortcutsFindFile, superClass); + const oldStopCallback = Mousetrap.stopCallback; + this.projectFindFile = projectFindFile; - function ShortcutsFindFile(projectFindFile) { - var _oldStopCallback; - this.projectFindFile = projectFindFile; - ShortcutsFindFile.__super__.constructor.call(this); - _oldStopCallback = Mousetrap.stopCallback; - Mousetrap.stopCallback = (function(_this) { - // override to fire shortcuts action when focus in textbox - return function(event, element, combo) { - if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) { - // when press up/down key in textbox, cusor prevent to move to home/end - event.preventDefault(); - return false; - } - return _oldStopCallback(event, element, combo); - }; - })(this); - Mousetrap.bind('up', this.projectFindFile.selectRowUp); - Mousetrap.bind('down', this.projectFindFile.selectRowDown); - Mousetrap.bind('esc', this.projectFindFile.goToTree); - Mousetrap.bind('enter', this.projectFindFile.goToBlob); - } + Mousetrap.stopCallback = (e, element, combo) => { + if ( + element === this.projectFindFile.inputElement[0] && + (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter') + ) { + // when press up/down key in textbox, cusor prevent to move to home/end + event.preventDefault(); + return false; + } - return ShortcutsFindFile; - })(ShortcutsNavigation); -}).call(window); + return oldStopCallback(e, element, combo); + }; + + Mousetrap.bind('up', this.projectFindFile.selectRowUp); + Mousetrap.bind('down', this.projectFindFile.selectRowDown); + Mousetrap.bind('esc', this.projectFindFile.goToTree); + Mousetrap.bind('enter', this.projectFindFile.goToBlob); + } +} diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 78b257bf192..fc97938e3d1 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,100 +1,74 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */ /* global Mousetrap */ -/* global ShortcutsNavigation */ /* global sidebar */ import _ from 'underscore'; import 'mousetrap'; -import './shortcuts_navigation'; - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.ShortcutsIssuable = (function(superClass) { - extend(ShortcutsIssuable, superClass); - - function ShortcutsIssuable(isMergeRequest) { - ShortcutsIssuable.__super__.constructor.call(this); - Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee')); - Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone')); - Mousetrap.bind('r', (function(_this) { - return function() { - _this.replyWithSelectedText(isMergeRequest); - return false; - }; - })(this)); - Mousetrap.bind('e', (function(_this) { - return function() { - _this.editIssue(); - return false; - }; - })(this)); - Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels')); - if (isMergeRequest) { - this.enabledHelp.push('.hidden-shortcut.merge_requests'); - } else { - this.enabledHelp.push('.hidden-shortcut.issues'); - } +import ShortcutsNavigation from './shortcuts_navigation'; + +export default class ShortcutsIssuable extends ShortcutsNavigation { + constructor(isMergeRequest) { + super(); + + this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); + this.editBtn = document.querySelector('.issuable-edit'); + + Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); + Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); + Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); + Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); + Mousetrap.bind('e', this.editIssue.bind(this)); + + if (isMergeRequest) { + this.enabledHelp.push('.hidden-shortcut.merge_requests'); + } else { + this.enabledHelp.push('.hidden-shortcut.issues'); } + } + + replyWithSelectedText() { + const documentFragment = window.gl.utils.getSelectedFragment(); + + if (!documentFragment) { + this.$replyField.focus(); + return false; + } + + const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); + const selected = window.gl.CopyAsGFM.nodeToGFM(el); - ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) { - var quote, documentFragment, el, selected, separator; - let replyField; - - if (isMergeRequest) { - replyField = $('.js-main-target-form #note_note'); - } else { - replyField = $('.js-main-target-form .js-vue-comment-form'); - } - - documentFragment = window.gl.utils.getSelectedFragment(); - if (!documentFragment) { - replyField.focus(); - return; - } - - el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); - selected = window.gl.CopyAsGFM.nodeToGFM(el); - - if (selected.trim() === "") { - return; - } - quote = _.map(selected.split("\n"), function(val) { - return ("> " + val).trim() + "\n"; - }); - - // If replyField already has some content, add a newline before our quote - separator = replyField.val().trim() !== "" && "\n\n" || ''; - replyField.val(function(a, current) { - return current + separator + quote.join('') + "\n"; - }); - - // Trigger autosave - replyField.trigger('input').trigger('change'); - - // Trigger autosize - var event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - replyField.get(0).dispatchEvent(event); - - // Focus the input field - return replyField.focus(); - }; - - ShortcutsIssuable.prototype.editIssue = function() { - var $editBtn; - $editBtn = $('.issuable-edit'); - // Need to click the element as on issues, editing is inline - // on merge request, editing is on a different page - $editBtn.get(0).click(); - }; - - ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { - sidebar.openDropdown(name); + if (selected.trim() === '') { return false; - }; + } + + const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`); + + // If replyField already has some content, add a newline before our quote + const separator = (this.$replyField.val().trim() !== '' && '\n\n') || ''; + this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`) + .trigger('input') + .trigger('change'); + + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + this.$replyField.get(0).dispatchEvent(event); + + // Focus the input field + this.$replyField.focus(); + + return false; + } + + editIssue() { + // Need to click the element as on issues, editing is inline + // on merge request, editing is on a different page + this.editBtn.click(); + + return false; + } - return ShortcutsIssuable; - })(ShortcutsNavigation); -}).call(window); + static openSidebarDropdown(name) { + sidebar.openDropdown(name); + return false; + } +} diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 55bae0c08a1..b4562701a3e 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,36 +1,27 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /* global Mousetrap */ -/* global Shortcuts */ import findAndFollowLink from './shortcuts_dashboard_navigation'; -import './shortcuts'; +import Shortcuts from './shortcuts'; -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; +export default class ShortcutsNavigation extends Shortcuts { + constructor() { + super(); - this.ShortcutsNavigation = (function(superClass) { - extend(ShortcutsNavigation, superClass); + Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); + Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); + Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); + Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); + Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); + Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network')); + Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts')); + Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); + Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); + Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); + Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); + Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); + Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); + Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); - function ShortcutsNavigation() { - ShortcutsNavigation.__super__.constructor.call(this); - Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); - Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); - Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); - Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); - Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); - Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network')); - Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts')); - Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); - Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); - Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); - Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); - Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); - Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); - Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); - this.enabledHelp.push('.hidden-shortcut.project'); - } - - return ShortcutsNavigation; - })(Shortcuts); -}).call(window); + this.enabledHelp.push('.hidden-shortcut.project'); + } +} diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index cc44082efa9..21823085ac4 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,28 +1,17 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */ /* global Mousetrap */ -/* global ShortcutsNavigation */ +import ShortcutsNavigation from './shortcuts_navigation'; -import './shortcuts_navigation'; +export default class ShortcutsNetwork extends ShortcutsNavigation { + constructor(graph) { + super(); -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; + Mousetrap.bind(['left', 'h'], graph.scrollLeft); + Mousetrap.bind(['right', 'l'], graph.scrollRight); + Mousetrap.bind(['up', 'k'], graph.scrollUp); + Mousetrap.bind(['down', 'j'], graph.scrollDown); + Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop); + Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom); - this.ShortcutsNetwork = (function(superClass) { - extend(ShortcutsNetwork, superClass); - - function ShortcutsNetwork(graph) { - this.graph = graph; - ShortcutsNetwork.__super__.constructor.call(this); - Mousetrap.bind(['left', 'h'], this.graph.scrollLeft); - Mousetrap.bind(['right', 'l'], this.graph.scrollRight); - Mousetrap.bind(['up', 'k'], this.graph.scrollUp); - Mousetrap.bind(['down', 'j'], this.graph.scrollDown); - Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop); - Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom); - this.enabledHelp.push('.hidden-shortcut.network'); - } - - return ShortcutsNetwork; - })(ShortcutsNavigation); -}).call(window); + this.enabledHelp.push('.hidden-shortcut.network'); + } +} diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js index 8a075062a48..59b967dbe09 100644 --- a/app/assets/javascripts/shortcuts_wiki.js +++ b/app/assets/javascripts/shortcuts_wiki.js @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this */ /* global Mousetrap */ -/* global ShortcutsNavigation */ +import ShortcutsNavigation from './shortcuts_navigation'; import findAndFollowLink from './shortcuts_dashboard_navigation'; export default class ShortcutsWiki extends ShortcutsNavigation { diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 9279b50cd55..7d8c5936b7d 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -16,6 +16,11 @@ export default { required: false, default: 'primary', }, + closeKind: { + type: String, + required: false, + default: 'default', + }, closeButtonLabel: { type: String, required: false, @@ -33,6 +38,11 @@ export default { [`btn-${this.kind}`]: true, }; }, + btnCancelKindClass() { + return { + [`btn-${this.closeKind}`]: true, + }; + }, }, methods: { @@ -70,7 +80,8 @@ export default { <div class="modal-footer"> <button type="button" - class="btn btn-default" + class="btn" + :class="btnCancelKindClass" @click="emitSubmit(false)"> {{closeButtonLabel}} </button> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 0d7a5cba928..aa61ddc6a2c 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -7,6 +7,7 @@ @import "framework/animations"; @import "framework/avatar"; @import "framework/asciidoctor"; +@import "framework/banner"; @import "framework/blocks"; @import "framework/buttons"; @import "framework/badges"; diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss new file mode 100644 index 00000000000..6433b0c7855 --- /dev/null +++ b/app/assets/stylesheets/framework/banner.scss @@ -0,0 +1,25 @@ +.banner-callout { + display: flex; + position: relative; + flex-wrap: wrap; + + .banner-close { + position: absolute; + top: 10px; + right: 10px; + opacity: 1; + + .dismiss-icon { + color: $gl-text-color; + font-size: $gl-font-size; + } + } + + .banner-graphic { + margin: 20px auto; + } + + &.banner-non-empty-state { + border-bottom: 1px solid $border-color; + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 9dcf332eee2..a9d804e735d 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -838,6 +838,7 @@ a { padding: 8px 40px; + &.is-indeterminate::before, &.is-active::before { left: 16px; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 588ec1ff3bc..5833ef939e9 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -10,6 +10,10 @@ border: 0; } + &.file-holder-bottom-radius { + border-radius: 0 0 $border-radius-small $border-radius-small; + } + &.readme-holder { margin: $gl-padding 0; diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 58f6e62b06a..50f1445bc2e 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -61,7 +61,6 @@ border: 1px solid $dropdown-border-color; min-width: 175px; color: $gl-text-color; - z-index: 999; } .select2-drop.select2-drop-above.select2-drop-active { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 089a67a7c98..d5ca23ff870 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -233,6 +233,7 @@ $container-text-max-width: 540px; $gl-avatar-size: 40px; $error-exclamation-point: $red-500; $border-radius-default: 4px; +$border-radius-small: 2px; $settings-icon-size: 18px; $provider-btn-not-active-color: $blue-500; $link-underline-blue: $blue-500; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index ffb5fc94475..09f831dcb29 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -707,11 +707,11 @@ .frame.click-to-comment { position: relative; - cursor: url(icon_image_comment.svg) + cursor: image-url('icon_image_comment.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; // Retina cursor - cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x) + cursor: -webkit-image-set(image-url('icon_image_comment.svg') 1x, image-url('icon_image_comment@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; .comment-indicator { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index d3cd4d507de..edfafa79c44 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -4,7 +4,7 @@ border-right: 1px solid $border-color; border-left: 1px solid $border-color; border-bottom: none; - border-radius: 2px; + border-radius: $border-radius-small $border-radius-small 0 0; background: $gray-normal; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index dae3ec7ac42..c93c4e93af5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -117,7 +117,7 @@ } .right-sidebar { - a, + a:not(.btn-retry), .btn-link { color: inherit; } @@ -459,7 +459,7 @@ } } - a { + a:not(.btn-retry) { &:hover { color: $md-link-color; text-decoration: none; diff --git a/app/controllers/projects/merge_requests/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb index 28afef101a9..366524b0783 100644 --- a/app/controllers/projects/merge_requests/conflicts_controller.rb +++ b/app/controllers/projects/merge_requests/conflicts_controller.rb @@ -53,7 +53,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' render json: { redirect_to: project_merge_request_url(@project, @merge_request, resolved_conflicts: true) } - rescue Gitlab::Conflict::ResolutionError => e + rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e render status: :bad_request, json: { message: e.message } end end diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index b517ddaebd7..9f403d96ed5 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -9,7 +9,7 @@ module TimeTrackable extend ActiveSupport::Concern included do - attr_reader :time_spent, :time_spent_user + attr_reader :time_spent, :time_spent_user, :spent_at alias_method :time_spent?, :time_spent @@ -24,6 +24,7 @@ module TimeTrackable def spend_time(options) @time_spent = options[:duration] @time_spent_user = options[:user] + @spent_at = options[:spent_at] @original_total_time_spent = nil return if @time_spent == 0 @@ -55,7 +56,11 @@ module TimeTrackable end def add_or_subtract_spent_time - timelogs.new(time_spent: time_spent, user: @time_spent_user) + timelogs.new( + time_spent: time_spent, + user: @time_spent_user, + spent_at: @spent_at + ) end def check_negative_time_spent diff --git a/app/models/gcp/cluster.rb b/app/models/gcp/cluster.rb index 18bd6a6dcb4..162a690c0e3 100644 --- a/app/models/gcp/cluster.rb +++ b/app/models/gcp/cluster.rb @@ -7,6 +7,9 @@ module Gcp belongs_to :user belongs_to :service + scope :enabled, -> { where(enabled: true) } + scope :disabled, -> { where(enabled: false) } + default_value_for :gcp_cluster_zone, 'us-central1-a' default_value_for :gcp_cluster_size, 3 default_value_for :gcp_machine_type, 'n1-standard-4' diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 972a35dde4d..c3fae16d109 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -396,7 +396,7 @@ class MergeRequest < ActiveRecord::Base end def merge_ongoing? - !!merge_jid && !merged? + !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) end def closed_without_fork? diff --git a/app/models/repository.rb b/app/models/repository.rb index bf526ca1762..4324ea46aac 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -468,9 +468,7 @@ class Repository end def blob_at(sha, path) - unless Gitlab::Git.blank_ref?(sha) - Blob.decorate(Gitlab::Git::Blob.find(self, sha, path), project) - end + Blob.decorate(raw_repository.blob_at(sha, path), project) rescue Gitlab::Git::Repository::NoRepository nil end @@ -914,14 +912,6 @@ class Repository end end - def resolve_conflicts(user, branch_name, params) - with_branch(user, branch_name) do - committer = user_to_committer(user) - - create_commit(params.merge(author: committer, committer: committer)) - end - end - def merged_to_root_ref?(branch_name) branch_commit = commit(branch_name) root_ref_commit = commit(root_ref) @@ -1127,7 +1117,7 @@ class Repository def last_commit_id_for_path_by_shelling_out(sha, path) args = %W(rev-list --max-count=1 #{sha} -- #{path}) - run_git(args).first.strip + raw_repository.run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip end def repository_storage_path diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index 9a636346899..f40cd2b06c8 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -56,11 +56,22 @@ module Auth def process_scope(scope) type, name, actions = scope.split(':', 3) actions = actions.split(',') - path = ContainerRegistry::Path.new(name) - return unless type == 'repository' + case type + when 'registry' + process_registry_access(type, name, actions) + when 'repository' + path = ContainerRegistry::Path.new(name) + process_repository_access(type, path, actions) + end + end + + def process_registry_access(type, name, actions) + return unless current_user&.admin? + return unless name == 'catalog' + return unless actions == ['*'] - process_repository_access(type, path, actions) + { type: type, name: name, actions: ['*'] } end def process_repository_access(type, path, actions) diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index d67b9f5cc56..c552193e66b 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -28,6 +28,8 @@ module Ci attributes.push([:user, current_user]) + build.retried = true + Ci::Build.transaction do # mark all other builds of that name as retried build.pipeline.builds.latest diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb index 727768b1a39..6805b2f7d1c 100644 --- a/app/services/merge_requests/add_todo_when_build_fails_service.rb +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -3,7 +3,7 @@ module MergeRequests # Adds a todo to the parent merge_request when a CI build fails # def execute(commit_status) - return if commit_status.allow_failure? + return if commit_status.allow_failure? || commit_status.retried? commit_status_merge_requests(commit_status) do |merge_request| todo_service.merge_request_build_failed(merge_request) diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb index 9835606812c..0f677a996f7 100644 --- a/app/services/merge_requests/conflicts/list_service.rb +++ b/app/services/merge_requests/conflicts/list_service.rb @@ -23,13 +23,13 @@ module MergeRequests # when there are no conflict files. conflicts.files.each(&:lines) @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 - rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing + rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing @conflicts_can_be_resolved_in_ui = false end end def conflicts - @conflicts ||= Gitlab::Conflict::FileCollection.read_only(merge_request) + @conflicts ||= Gitlab::Conflict::FileCollection.new(merge_request) end end end diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb index 6b6e231f4f9..27cafd2d7d9 100644 --- a/app/services/merge_requests/conflicts/resolve_service.rb +++ b/app/services/merge_requests/conflicts/resolve_service.rb @@ -1,54 +1,10 @@ module MergeRequests module Conflicts class ResolveService < MergeRequests::Conflicts::BaseService - MissingFiles = Class.new(Gitlab::Conflict::ResolutionError) - def execute(current_user, params) - rugged = merge_request.source_project.repository.rugged - - Gitlab::Conflict::FileCollection.for_resolution(merge_request) do |conflicts_for_resolution| - merge_index = conflicts_for_resolution.merge_index - - params[:files].each do |file_params| - conflict_file = conflicts_for_resolution.file_for_path(file_params[:old_path], file_params[:new_path]) - - write_resolved_file_to_index(merge_index, rugged, conflict_file, file_params) - end - - unless merge_index.conflicts.empty? - missing_files = merge_index.conflicts.map { |file| file[:ours][:path] } - - raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}" - end - - commit_params = { - message: params[:commit_message] || conflicts_for_resolution.default_commit_message, - parents: [conflicts_for_resolution.our_commit, conflicts_for_resolution.their_commit].map(&:oid), - tree: merge_index.write_tree(rugged) - } - - conflicts_for_resolution - .project - .repository - .resolve_conflicts(current_user, merge_request.source_branch, commit_params) - end - end - - private - - def write_resolved_file_to_index(merge_index, rugged, file, params) - if params[:sections] - new_file = file.resolve_lines(params[:sections]).map(&:text).join("\n") - - new_file << "\n" if file.our_blob.data.ends_with?("\n") - elsif params[:content] - new_file = file.resolve_content(params[:content]) - end - - our_path = file.our_path + conflicts = Gitlab::Conflict::FileCollection.new(merge_request) - merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) - merge_index.conflict_remove(our_path) + conflicts.resolve(current_user, params[:commit_message], params[:files]) end end end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 955d934838b..06ac86cd5a9 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -381,7 +381,7 @@ module QuickActions end desc 'Add or substract spent time' - explanation do |time_spent| + explanation do |time_spent, time_spent_date| if time_spent if time_spent > 0 verb = 'Adds' @@ -394,16 +394,20 @@ module QuickActions "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time." end end - params '<1h 30m | -1h 30m>' + params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) end - parse_params do |raw_duration| - Gitlab::TimeTrackingFormatter.parse(raw_duration) + parse_params do |raw_time_date| + Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute end - command :spend do |time_spent| + command :spend do |time_spent, time_spent_date| if time_spent - @updates[:spend_time] = { duration: time_spent, user: current_user } + @updates[:spend_time] = { + duration: time_spent, + user: current_user, + spent_at: time_spent_date + } end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index a52dce6cb4b..0bce20ae5b7 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -195,9 +195,11 @@ module SystemNoteService if time_spent == :reset body = "removed time spent" else + spent_at = noteable.spent_at parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) action = time_spent > 0 ? 'added' : 'subtracted' body = "#{action} #{parsed_time} of time spent" + body << " at #{spent_at}" if spent_at end create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index b18b3dd5766..29b23ae2e52 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -17,10 +17,6 @@ %th Global Shortcuts %tr %td.shortcut - .key n - %td Main Navigation - %tr - %td.shortcut .key s %td Focus Search %tr diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 4b344b2edb9..7777f55ddd7 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,6 +1,6 @@ - action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create' -.file-holder.file.append-bottom-default +.file-holder-bottom-radius.file-holder.file.append-bottom-default .js-file-title.file-title.clearfix{ data: { current_action: action } } .editor-ref = icon('code-fork') diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 3f3ce10419f..c9956183e12 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -24,10 +24,15 @@ %p You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected. + - if show_auto_devops_callout?(@project) + %p + - link = link_to(s_('AutoDevOps|Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) + = s_('AutoDevOps|You can activate %{link_to_settings} for this project.').html_safe % { link_to_settings: link } + %p + = s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') + - if can?(current_user, :push_code, @project) %div{ class: container_class } - - if show_auto_devops_callout?(@project) - = render 'shared/auto_devops_callout' .prepend-top-20 .empty_wrapper %h3.page-title-empty diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index d5c6d329ce4..7da4ffd5e43 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -4,8 +4,10 @@ .sidebar-container .blocks-container .block - %strong + %strong.prepend-top-10 = @build.name + - if can?(current_user, :update_build, @build) && @build.retryable? + = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' } = icon('angle-double-right') diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 6b8dcb3e60b..8da2243adef 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -13,8 +13,6 @@ - if @project.merge_requests.exists? %div{ class: container_class } - - if show_auto_devops_callout?(@project) - = render 'shared/auto_devops_callout' .top-area = render 'shared/issuable/nav', type: :merge_requests .nav-controls diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 0a835dcdeb0..0a7880ce4cd 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -54,6 +54,10 @@ = f.label :visibility_level, class: 'label-light' do #the label here seems wrong Import project from .import-buttons + - if gitlab_project_import_enabled? + .import_gitlab_project.has-tooltip{ data: { container: 'body' } } + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = icon('gitlab', text: 'GitLab export') %div - if github_import_enabled? = link_to new_import_github_path, class: 'btn import_github' do @@ -87,10 +91,6 @@ - if git_import_enabled? %button.btn.js-toggle-button.import_git{ type: "button" } = icon('git', text: 'Repo by URL') - - if gitlab_project_import_enabled? - .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - = icon('gitlab', text: 'GitLab export') .col-lg-12 .js-toggle-content.hide.toggle-import-form %hr diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index a10a7c23924..f8627a3818b 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -2,8 +2,6 @@ - page_title "Pipelines" %div{ 'class' => container_class } - - if show_auto_devops_callout?(@project) - = render 'shared/auto_devops_callout' #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), "help-page-path" => help_page_path('ci/quick_start/README'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 0cc6674842a..745a6040488 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -12,7 +12,5 @@ = webpack_bundle_tag 'repo' %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - - if show_auto_devops_callout?(@project) && !show_new_repo? - = render 'shared/auto_devops_callout' = render 'projects/last_push' = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index 7c633175a06..934d65e8b42 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -1,15 +1,16 @@ -.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } - .bordered-box.landing.content-block - %button.btn.btn-default.close.js-close-callout{ type: 'button', - 'aria-label' => 'Dismiss Auto DevOps box' } - = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') - .svg-container - = custom_icon('icon_autodevops') - .user-callout-copy - %h4= s_('AutoDevOps|Auto DevOps (Beta)') - %p= s_('AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') - %p - - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') - = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } +.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } + .banner-graphic + = custom_icon('icon_autodevops') - = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout' + .prepend-top-10.prepend-left-10.append-bottom-10 + %h5= s_('AutoDevOps|Auto DevOps (Beta)') + %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') + %p + - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') + = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } + .prepend-top-10 + = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' + + %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', + 'aria-label' => 'Dismiss Auto DevOps box' } + = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 1f540bdaf93..dfc0f9be321 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -25,7 +25,7 @@ show_any: "true", project_id: @project&.try(:id), labels: labels_filter_path(false), - namespace_path: @project.try(:namespace).try(:full_path), + namespace_path: @namespace_path, project_path: @project.try(:path) }, ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" } %span.dropdown-toggle-text diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg index 807ff27bb67..7e47c084bde 100644 --- a/app/views/shared/icons/_icon_autodevops.svg +++ b/app/views/shared/icons/_icon_autodevops.svg @@ -1,4 +1,4 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="189" height="179" viewBox="0 0 189 179"> +<svg xmlns="http://www.w3.org/2000/svg" width="189" height="110" viewBox="0 0 189 179"> <g fill="none" fill-rule="evenodd"> <path fill="#FFFFFF" fill-rule="nonzero" d="M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/> <path fill="#EEEEEE" fill-rule="nonzero" d="M110.160166,51.6956996 C106.846457,51.6956996 104.160166,54.3819911 104.160166,57.6956996 L104.160166,117.6957 C104.160166,121.009408 106.846457,123.6957 110.160166,123.6957 L160.160166,123.6957 C163.473874,123.6957 166.160166,121.009408 166.160166,117.6957 L166.160166,57.6956996 C166.160166,54.3819911 163.473874,51.6956996 160.160166,51.6956996 L110.160166,51.6956996 Z M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/> diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 87fa2007d16..919f19f2c23 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -3,5 +3,7 @@ refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, + blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'), + new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }), can_commit: (!!can_push_branch?(project, @ref)).to_s, on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } diff --git a/bin/changelog b/bin/changelog index 61d4de06e90..efe25032ba1 100755 --- a/bin/changelog +++ b/bin/changelog @@ -28,6 +28,7 @@ class ChangelogOptionParser Type.new('deprecated', 'New deprecation'), Type.new('removed', 'Feature removal'), Type.new('security', 'Security fix'), + Type.new('performance', 'Performance improvement'), Type.new('other', 'Other') ].freeze TYPES_OFFSET = 1 diff --git a/changelogs/unreleased/1312-time-spent-at.yml b/changelogs/unreleased/1312-time-spent-at.yml new file mode 100644 index 00000000000..c029497e9ab --- /dev/null +++ b/changelogs/unreleased/1312-time-spent-at.yml @@ -0,0 +1,5 @@ +--- +title: Added possibility to enter past date in /spend command to log time in the past +merge_request: 3044 +author: g3dinua, LockiStrike +type: changed diff --git a/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml b/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml new file mode 100644 index 00000000000..8918c42e3fb --- /dev/null +++ b/changelogs/unreleased/26763-grant-registry-auth-scope-to-admins.yml @@ -0,0 +1,5 @@ +--- +title: Issue JWT token with registry:catalog:* scope when requested by GitLab admin +merge_request: 14751 +author: Vratislav Kalenda +type: added diff --git a/changelogs/unreleased/27654-retry-button.yml b/changelogs/unreleased/27654-retry-button.yml new file mode 100644 index 00000000000..11f3b5eb779 --- /dev/null +++ b/changelogs/unreleased/27654-retry-button.yml @@ -0,0 +1,5 @@ +--- +title: Move retry button in job page to sidebar +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/36160-select2-dropdown.yml b/changelogs/unreleased/36160-select2-dropdown.yml deleted file mode 100644 index a836744fb41..00000000000 --- a/changelogs/unreleased/36160-select2-dropdown.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Decreases z-index of select2 to a lower number of our navigation bar -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml b/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml new file mode 100644 index 00000000000..bc93aa1fca4 --- /dev/null +++ b/changelogs/unreleased/37571-replace-wikipage-createservice-with-factory.yml @@ -0,0 +1,5 @@ +--- +title: Replace WikiPage::CreateService calls with wiki_page factory in specs +merge_request: 14850 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml b/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml new file mode 100644 index 00000000000..554249a3f88 --- /dev/null +++ b/changelogs/unreleased/37978-extra-border-radius-while-editing-a-file.yml @@ -0,0 +1,6 @@ +--- +title: Removed extra border radius from .file-editor and .file-holder when editing + a file +merge_request: 14803 +author: Rachel Pipkin +type: fixed diff --git a/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml b/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml new file mode 100644 index 00000000000..48b92c02505 --- /dev/null +++ b/changelogs/unreleased/38236-remove-build-failed-todo-if-it-has-been-auto-retried.yml @@ -0,0 +1,5 @@ +--- +title: Don't create build failed todos when the job is automatically retried +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39017-gitlabusagepingworker-is-not-running-on-gitlab-com.yml b/changelogs/unreleased/39017-gitlabusagepingworker-is-not-running-on-gitlab-com.yml new file mode 100644 index 00000000000..89506f88637 --- /dev/null +++ b/changelogs/unreleased/39017-gitlabusagepingworker-is-not-running-on-gitlab-com.yml @@ -0,0 +1,5 @@ +--- +title: Make usage ping scheduling more robust +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39032-improve-merge-ongoing-check-consistency.yml b/changelogs/unreleased/39032-improve-merge-ongoing-check-consistency.yml new file mode 100644 index 00000000000..361b6af196a --- /dev/null +++ b/changelogs/unreleased/39032-improve-merge-ongoing-check-consistency.yml @@ -0,0 +1,5 @@ +--- +title: Make "merge ongoing" check more consistent +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml b/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml new file mode 100644 index 00000000000..4b90d68d80c --- /dev/null +++ b/changelogs/unreleased/39035-move-gitlab-export-to-top-import-list.yml @@ -0,0 +1,5 @@ +--- +title: 14830 Move GitLab export option to top of import list when creating a new project +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/an-popen-deadline.yml b/changelogs/unreleased/an-popen-deadline.yml new file mode 100644 index 00000000000..4b74c63ed5c --- /dev/null +++ b/changelogs/unreleased/an-popen-deadline.yml @@ -0,0 +1,5 @@ +--- +title: Use a timeout on certain git operations +merge_request: 14872 +author: +type: security diff --git a/changelogs/unreleased/es-module-broadcast_message.yml b/changelogs/unreleased/es-module-broadcast_message.yml new file mode 100644 index 00000000000..031bcc449ae --- /dev/null +++ b/changelogs/unreleased/es-module-broadcast_message.yml @@ -0,0 +1,5 @@ +--- +title: Fix unnecessary ajax requests in admin broadcast message form +merge_request: 14853 +author: +type: fixed diff --git a/changelogs/unreleased/fix_diff_parsing.yml b/changelogs/unreleased/fix_diff_parsing.yml new file mode 100644 index 00000000000..7a26b4f9ff5 --- /dev/null +++ b/changelogs/unreleased/fix_diff_parsing.yml @@ -0,0 +1,5 @@ +--- +title: Fix diff parser so it tolerates to diff special markers in the content +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix_global_board_routes_39073.yml b/changelogs/unreleased/fix_global_board_routes_39073.yml new file mode 100644 index 00000000000..cc9ae8592db --- /dev/null +++ b/changelogs/unreleased/fix_global_board_routes_39073.yml @@ -0,0 +1,5 @@ +--- +title: Allow boards as top level route +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fl-autodevops-fix.yml b/changelogs/unreleased/fl-autodevops-fix.yml new file mode 100644 index 00000000000..21b739231a8 --- /dev/null +++ b/changelogs/unreleased/fl-autodevops-fix.yml @@ -0,0 +1,5 @@ +--- +title: Improve autodevops banner UX and render it only in project page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/new-mr-repo-editor.yml b/changelogs/unreleased/new-mr-repo-editor.yml new file mode 100644 index 00000000000..a6c15ee30a9 --- /dev/null +++ b/changelogs/unreleased/new-mr-repo-editor.yml @@ -0,0 +1,5 @@ +--- +title: 'Repo Editor: Add option to start a new MR directly from comit section' +merge_request: 14665 +author: +type: added diff --git a/changelogs/unreleased/sha-handling.yml b/changelogs/unreleased/sha-handling.yml new file mode 100644 index 00000000000..d776edafef5 --- /dev/null +++ b/changelogs/unreleased/sha-handling.yml @@ -0,0 +1,5 @@ +--- +title: Fix 404 errors in API caused when the branch name had a dot +merge_request: 14462 +author: gvieira37 +type: fixed diff --git a/changelogs/unreleased/winh-indeterminate-dropdown.yml b/changelogs/unreleased/winh-indeterminate-dropdown.yml new file mode 100644 index 00000000000..61205d1643e --- /dev/null +++ b/changelogs/unreleased/winh-indeterminate-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Fix alignment for indeterminate marker in dropdowns +merge_request: 14809 +author: +type: fixed diff --git a/changelogs/unreleased/zj-add-performance-changelog-cat.yml b/changelogs/unreleased/zj-add-performance-changelog-cat.yml new file mode 100644 index 00000000000..3d58044a254 --- /dev/null +++ b/changelogs/unreleased/zj-add-performance-changelog-cat.yml @@ -0,0 +1,5 @@ +--- +title: Add Performance improvement as category on the changelog +merge_request: +author: +type: performance diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index a4b7c1a3919..b790df565c6 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -113,12 +113,14 @@ class Settings < Settingslogic URI.parse(url_without_path).host end - # Random cron time every Sunday to load balance usage pings - def cron_random_weekly_time + # Runs every minute in a random ten-minute period on Sundays, to balance the + # load on the server receiving these pings. The usage ping is safe to run + # multiple times because of a 24 hour exclusive lock. + def cron_for_usage_ping hour = rand(24) - minute = rand(60) + minute = rand(6) - "#{minute} #{hour} * * 0" + "#{minute}0-#{minute}9 #{hour} * * 0" end end end @@ -398,7 +400,7 @@ Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *' Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker' Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({}) -Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_random_weekly_time) +Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= Settings.__send__(:cron_for_usage_ping) Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker' Settings.cron_jobs['schedule_update_user_activity_worker'] ||= Settingslogic.new({}) diff --git a/config/routes.rb b/config/routes.rb index 405bfcc2d8e..fc13dc4865f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,6 +44,19 @@ Rails.application.routes.draw do get 'readiness' => 'health#readiness' resources :metrics, only: [:index] mount Peek::Railtie => '/peek' + + # Boards resources shared between group and projects + resources :boards, only: [] do + resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do + collection do + post :generate + end + + resources :issues, only: [:index, :create, :update] + end + + resources :issues, module: :boards, only: [:index, :update] + end end # Koding route @@ -74,19 +87,6 @@ Rails.application.routes.draw do # Notification settings resources :notification_settings, only: [:create, :update] - # Boards resources shared between group and projects - resources :boards do - resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do - collection do - post :generate - end - - resources :issues, only: [:index, :create, :update] - end - - resources :issues, module: :boards, only: [:index, :update] - end - draw :google_api draw :import draw :uploads diff --git a/config/webpack.config.js b/config/webpack.config.js index a71794b379d..f7a7182a627 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -236,7 +236,7 @@ var config = { from: path.join(ROOT_PATH, `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`), to: 'monaco-editor/vs', transform: function(content, path) { - if (/\.js$/.test(path) && !/worker/i.test(path)) { + if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) { return ( '(function(){\n' + 'var define = this.define, require = this.require;\n' + diff --git a/db/migrate/20170909150936_add_spent_at_to_timelogs.rb b/db/migrate/20170909150936_add_spent_at_to_timelogs.rb new file mode 100644 index 00000000000..ffff719c289 --- /dev/null +++ b/db/migrate/20170909150936_add_spent_at_to_timelogs.rb @@ -0,0 +1,11 @@ +class AddSpentAtToTimelogs < ActiveRecord::Migration + DOWNTIME = false + + def up + add_column :timelogs, :spent_at, :datetime_with_timezone + end + + def down + remove_column :timelogs, :spent_at + end +end diff --git a/db/schema.rb b/db/schema.rb index aac37b6b455..8aadcfeb7d1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1550,6 +1550,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do t.datetime "updated_at", null: false t.integer "issue_id" t.integer "merge_request_id" + t.datetime_with_timezone "spent_at" end add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree diff --git a/doc/api/repositories.md b/doc/api/repositories.md index bccef924375..594babc74be 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -85,7 +85,7 @@ GET /projects/:id/repository/blobs/:sha Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user -- `sha` (required) - The commit or branch name +- `sha` (required) - The blob SHA ## Raw blob content diff --git a/doc/api/settings.md b/doc/api/settings.md index b78f1252108..be75e1b66ba 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -64,38 +64,89 @@ PUT /application/settings | Attribute | Type | Required | Description | | --------- | ---- | :------: | ----------- | -| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` | -| `signup_enabled` | boolean | no | Enable registration. Default is `true`. | -| `password_authentication_enabled` | boolean | no | Enable authentication via a GitLab account password. Default is `true`. | -| `gravatar_enabled` | boolean | no | Enable Gravatar | -| `sign_in_text` | string | no | Text on login page | -| `home_page_url` | string | no | Redirect to this URL when not logged in | -| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. | -| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. | -| `max_attachment_size` | integer | no | Limit attachment size in MB | -| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | -| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.| -| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.| -| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`.| -| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. | -| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` | -| `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. | -| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | -| `after_sign_out_path` | string | no | Where to redirect users after logout | -| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | -| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. | -| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | -| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. | -| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. | -| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources | -| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. | -| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. | -| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. | -| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling. | -| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. -| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. -| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. -| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. +| `admin_notification_email` | string | no | Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. | +| `after_sign_out_path` | string | no | Where to redirect users after logout | +| `after_sign_up_text` | string | no | Text shown to the user after signing up | +| `akismet_api_key` | string | no | API key for akismet spam protection | +| `akismet_enabled` | boolean | no | Enable or disable akismet spam protection | +| `clientside_sentry_dsn` | string | no | Required if `clientside_sentry_dsn` is enabled | +| `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side | +| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | +| `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts | +| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `2`. | +| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` | +| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | +| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources | +| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` | +| `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. | +| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. | +| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. | +| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. | +| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. | +| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. | +| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | +| `gravatar_enabled` | boolean | no | Enable Gravatar | +| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help | +| `help_page_support_url` | string | no | Alternate support URL for help page | +| `home_page_url` | string | no | Redirect to this URL when not logged in | +| `housekeeping_bitmaps_enabled` | boolean | no | Enable Git pack file bitmap creation | +| `housekeeping_enabled` | boolean | no | Enable or disable git housekeeping | +| `housekeeping_full_repack_period` | integer | no | Number of Git pushes after which an incremental 'git repack' is run. | +| `housekeeping_gc_period` | integer | no | Number of Git pushes after which 'git gc' is run. | +| `housekeeping_incremental_repack_period` | integer | no | Number of Git pushes after which an incremental 'git repack' is run. | +| `html_emails_enabled` | boolean | no | Enable HTML emails | +| `import_sources` | Array of strings | no | Sources to allow project import from, possible values: "github bitbucket gitlab google_code fogbugz git gitlab_project | +| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. | +| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. | +| `max_artifacts_size` | integer | no | Maximum artifacts size in MB | +| `max_attachment_size` | integer | no | Limit attachment size in MB | +| `max_pages_size` | integer | no | Maximum size of pages repositories in MB | +| `metrics_enabled` | boolean | no | Enable influxDB metrics | +| `metrics_host` | string | yes (if `metrics_enabled` is `true`) | InfluxDB host | +| `metrics_method_call_threshold` | integer | yes (if `metrics_enabled` is `true`) | A method call is only tracked when it takes longer than the given amount of milliseconds | +| `metrics_packet_size` | integer | yes (if `metrics_enabled` is `true`) | The amount of datapoints to send in a single UDP packet. | +| `metrics_pool_size` | integer | yes (if `metrics_enabled` is `true`) | The amount of InfluxDB connections to keep open | +| `metrics_port` | integer | no | The UDP port to use for connecting to InfluxDB | +| `metrics_sample_interval` | integer | yes (if `metrics_enabled` is `true`) | The sampling interval in seconds. | +| `metrics_timeout` | integer | yes (if `metrics_enabled` is `true`) | The amount of seconds after which InfluxDB will time out. | +| `password_authentication_enabled` | boolean | no | Enable authentication via a GitLab account password. Default is `true`. | +| `performance_bar_allowed_group_id` | string | no | The group that is allowed to enable the performance bar | +| `performance_bar_enabled` | boolean | no | Allow enabling the performance bar | +| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. | +| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. | +| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling. | +| `project_export_enabled` | boolean | no | Enable project export | +| `prometheus_metrics_enabled` | boolean | no | Enable prometheus metrics | +| `recaptcha_enabled` | boolean | no | Enable recaptcha | +| `recaptcha_private_key` | string | yes (if `recaptcha_enabled` is true) | Private key for recaptcha | +| `recaptcha_site_key` | string | yes (if `recaptcha_enabled` is true) | Site key for recaptcha | +| `repository_checks_enabled` | boolean | no | GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. | +| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. | +| `require_two_factor_authentication` | boolean | no | Require all users to setup Two-factor authentication | +| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. | +| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. | +| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up | +| `sentry_dsn` | string | yes (if `sentry_enabled` is true) | Sentry Data Source Name | +| `sentry_enabled` | boolean | no | Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com | +| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | +| `shared_runners_enabled` | true | no | Enable shared runners for new projects | +| `shared_runners_text` | string | no | Shared runners text | +| `sidekiq_throttling_enabled` | boolean | no | Enable Sidekiq Job Throttling | +| `sidekiq_throttling_factor` | decimal | yes (if `sidekiq_throttling_enabled` is true) | The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. | +| `sidekiq_throttling_queues` | array of strings | yes (if `sidekiq_throttling_enabled` is true) | Choose which queues you wish to throttle | +| `sign_in_text` | string | no | Text on login page | +| `signup_enabled` | boolean | no | Enable registration. Default is `true`. | +| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. | +| `two_factor_grace_period` | integer | no | Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication | +| `unique_ips_limit_enabled` | boolean | no | Limit sign in from multiple ips | +| `unique_ips_limit_per_user` | integer | yes (if `unique_ips_limit_enabled` is true) | Maximum number of ips per user | +| `unique_ips_limit_time_window` | integer | yes (if `unique_ips_limit_enabled` is true) | How many seconds an IP will be counted towards the limit | +| `usage_ping_enabled` | boolean | no | Every week GitLab will report license usage back to GitLab, Inc. | +| `user_default_external` | boolean | no | Newly registered users will by default be external | +| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | +| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal diff --git a/doc/ci/README.md b/doc/ci/README.md index 5cfd82de381..ec0ddfbea75 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -42,7 +42,7 @@ digging into specific reference guides. - **The permissions model** - Learn about the access levels a user can have for performing certain CI actions - [User permissions](../user/permissions.md#gitlab-ci) - - [Jobs permissions](../user/permissions.md#jobs-permissions) + - [Job permissions](../user/permissions.md#job-permissions) ## Auto DevOps diff --git a/doc/development/profiling.md b/doc/development/profiling.md index 933033a09e0..af79353b721 100644 --- a/doc/development/profiling.md +++ b/doc/development/profiling.md @@ -27,3 +27,13 @@ Bullet will log query problems to both the Rails log as well as the Chrome console. As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression. + +## GitLab Profiler + + +[Gitlab-Profiler](https://gitlab.com/gitlab-com/gitlab-profiler) was built to +help developers understand why specific URLs of their application may be slow +and to provide hard data that can help reduce load times. + +For GitLab.com, you can find the latest results here: +<http://redash.gitlab.com/dashboard/gitlab-profiler-statistics> diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 67ef189fee9..e18711f3392 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -17,7 +17,7 @@ [Project Templates](https://gitlab.com/gitlab-org/project-templates): this will kickstart your repository code and CI automatically. Otherwise, if you have a project in a different repository, you can [import it] by - clicking an **Import project from** button provided this is enabled in + clicking on the **Import project** tab, provided this is enabled in your GitLab instance. Ask your administrator if not. 1. Provide the following information: diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png Binary files differindex ef8753e224b..ce4f7d1204b 100644 --- a/doc/gitlab-basics/img/create_new_project_info.png +++ b/doc/gitlab-basics/img/create_new_project_info.png diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 17fe80fa93d..3d7becd18fc 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -121,8 +121,8 @@ Existing users using GitLab with MySQL/MariaDB are advised to ### PostgreSQL Requirements -As of GitLab 9.3, PostgreSQL 9.2 or newer is required, and earlier versions are -not supported. We highly recommend users to use at least PostgreSQL 9.6 as this +As of GitLab 10.0, PostgreSQL 9.6 or newer is required, and earlier versions are +not supported. We highly recommend users to use PostgreSQL 9.6 as this is the PostgreSQL version used for development and testing. Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md index d6f3a7d5555..10dede255ec 100644 --- a/doc/user/project/issues/automatic_issue_closing.md +++ b/doc/user/project/issues/automatic_issue_closing.md @@ -19,7 +19,7 @@ When not specified, the default issue closing pattern as shown below will be used: ```bash -((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) ``` Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's @@ -34,6 +34,7 @@ This translates to the following keywords: - Close, Closes, Closed, Closing, close, closes, closed, closing - Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing - Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving +- Implement, Implements, Implemented, Implementing, implement, implements, implemented, implementing --- diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 6a5d2d40927..e81e935e37d 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -32,7 +32,7 @@ do. | `/wip` | Toggle the Work In Progress status | | <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | | `/remove_estimate` | Remove estimated time | -| <code>/spend <1h 30m | -1h 5m></code> | Add or subtract spent time | +| <code>/spend <time(1h 30m | -1h 5m)> <date(YYYY-MM-DD)></code> | Add or subtract spent time; optionally, specify the date that time was spent on | | `/remove_time_spent` | Remove time spent | | `/target_branch <Branch Name>` | Set target branch for current merge request | | `/award :emoji:` | Toggle award for :emoji: | diff --git a/lib/api/api.rb b/lib/api/api.rb index 79e55a2f4f7..99fcc59ba04 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -4,6 +4,10 @@ module API LOG_FILENAME = Rails.root.join("log", "api_json.log") + NO_SLASH_URL_PART_REGEX = %r{[^/]+} + PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze + COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze + use GrapeLogging::Middleware::RequestLogger, logger: Logger.new(LOG_FILENAME), formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, @@ -96,9 +100,6 @@ module API helpers ::API::Helpers helpers ::API::Helpers::CommonHelpers - NO_SLASH_URL_PART_REGEX = %r{[^/]+} - PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze - # Keep in alphabetical order mount ::API::AccessRequests mount ::API::AwardEmoji diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 4af37a2ad1d..2685dc27252 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -4,8 +4,6 @@ module API class Commits < Grape::API include PaginationParams - COMMIT_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: API::NO_SLASH_URL_PART_REGEX) - before { authorize! :download_code, user_project } params do @@ -85,7 +83,7 @@ module API params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' end - get ':id/repository/commits/:sha', requirements: COMMIT_ENDPOINT_REQUIREMENTS do + get ':id/repository/commits/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit @@ -99,7 +97,7 @@ module API params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' end - get ':id/repository/commits/:sha/diff', requirements: COMMIT_ENDPOINT_REQUIREMENTS do + get ':id/repository/commits/:sha/diff', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit @@ -115,7 +113,7 @@ module API use :pagination requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' end - get ':id/repository/commits/:sha/comments', requirements: COMMIT_ENDPOINT_REQUIREMENTS do + get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit @@ -132,7 +130,7 @@ module API requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked' requires :branch, type: String, desc: 'The name of the branch' end - post ':id/repository/commits/:sha/cherry_pick', requirements: COMMIT_ENDPOINT_REQUIREMENTS do + post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do authorize! :push_code, user_project commit = user_project.commit(params[:sha]) @@ -169,7 +167,7 @@ module API requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line' end end - post ':id/repository/commits/:sha/comments', requirements: COMMIT_ENDPOINT_REQUIREMENTS do + post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit @@ -186,7 +184,7 @@ module API lines.each do |line| next unless line.new_pos == params[:line] && line.type == params[:line_type] - break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) end break if opts[:line_code] diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index ceee3226732..7887b886c03 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -57,7 +57,7 @@ module API desc 'Get raw blob contents from the repository' params do - requires :sha, type: String, desc: 'The commit, branch name, or tag name' + requires :sha, type: String, desc: 'The commit hash' end get ':id/repository/blobs/:sha/raw' do assign_blob_vars! @@ -67,7 +67,7 @@ module API desc 'Get a blob from the repository' params do - requires :sha, type: String, desc: 'The commit, branch name, or tag name' + requires :sha, type: String, desc: 'The commit hash' end get ':id/repository/blobs/:sha' do assign_blob_vars! diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb index c189d486f50..f493fd7c7ec 100644 --- a/lib/api/v3/builds.rb +++ b/lib/api/v3/builds.rb @@ -8,7 +8,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do helpers do params :optional_scope do optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index 345cb7e7c11..ed206a6def0 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -11,7 +11,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: { id: %r{[^/]+} } do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do desc 'Get a project repository commits' do success ::API::Entities::Commit end @@ -72,7 +72,7 @@ module API params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' end - get ":id/repository/commits/:sha" do + get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! "Commit" unless commit @@ -86,7 +86,7 @@ module API params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' end - get ":id/repository/commits/:sha/diff" do + get ":id/repository/commits/:sha/diff", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! "Commit" unless commit @@ -102,7 +102,7 @@ module API use :pagination requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' end - get ':id/repository/commits/:sha/comments' do + get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit @@ -119,7 +119,7 @@ module API requires :sha, type: String, desc: 'A commit sha to be cherry picked' requires :branch, type: String, desc: 'The name of the branch' end - post ':id/repository/commits/:sha/cherry_pick' do + post ':id/repository/commits/:sha/cherry_pick', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do authorize! :push_code, user_project commit = user_project.commit(params[:sha]) @@ -156,7 +156,7 @@ module API requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line' end end - post ':id/repository/commits/:sha/comments' do + post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit @@ -173,7 +173,7 @@ module API lines.each do |line| next unless line.new_pos == params[:line] && line.type == params[:line_type] - break opts[:line_code] = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + break opts[:line_code] = Gitlab::Git.diff_line_code(diff.new_path, line.new_pos, line.old_pos) end break if opts[:line_code] diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb index 41a7c6b83ae..f9a47101e27 100644 --- a/lib/api/v3/repositories.rb +++ b/lib/api/v3/repositories.rb @@ -8,7 +8,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: { id: %r{[^/]+} } do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do helpers do def handle_project_member_errors(errors) if errors[:project_access].any? @@ -43,7 +43,7 @@ module API requires :sha, type: String, desc: 'The commit, branch name, or tag name' requires :filepath, type: String, desc: 'The path to the file to display' end - get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do + get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"], requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do repo = user_project.repository commit = repo.commit(params[:sha]) not_found! "Commit" unless commit @@ -56,7 +56,7 @@ module API params do requires :sha, type: String, desc: 'The commit, branch name, or tag name' end - get ':id/repository/raw_blobs/:sha' do + get ':id/repository/raw_blobs/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do repo = user_project.repository begin blob = Gitlab::Git::Blob.raw(repo, params[:sha]) diff --git a/lib/github/representation/comment.rb b/lib/github/representation/comment.rb index 1b5be91461b..83bf0b5310d 100644 --- a/lib/github/representation/comment.rb +++ b/lib/github/representation/comment.rb @@ -23,7 +23,7 @@ module Github private def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos) end def on_diff? diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index d1979bb7ed3..033ecd15749 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -241,7 +241,7 @@ module Gitlab end def generate_line_code(pr_comment) - Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos) + Gitlab::Git.diff_line_code(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos) end def pull_request_comment_attributes(comment) diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index 98dfe900044..2a0cb640a14 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -4,82 +4,29 @@ module Gitlab include Gitlab::Routing include IconsHelper - MissingResolution = Class.new(ResolutionError) - CONTEXT_LINES = 3 - attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository - - def initialize(merge_file_result, conflict, merge_request:) - @merge_file_result = merge_file_result - @their_path = conflict[:theirs][:path] - @our_path = conflict[:ours][:path] - @our_mode = conflict[:ours][:mode] - @merge_request = merge_request - @repository = merge_request.project.repository - @match_line_headers = {} - end - - def content - merge_file_result[:data] - end + attr_reader :merge_request - def our_blob - @our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path) - end + # 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps + attr_reader :raw - def type - lines unless @type + delegate :type, :content, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw - @type.inquiry + def initialize(raw, merge_request:) + @raw = raw + @merge_request = merge_request + @match_line_headers = {} end - # Array of Gitlab::Diff::Line objects def lines return @lines if defined?(@lines) - begin - @type = 'text' - @lines = Gitlab::Conflict::Parser.new.parse(content, - our_path: our_path, - their_path: their_path, - parent_file: self) - rescue Gitlab::Conflict::Parser::ParserError - @type = 'text-editor' - @lines = nil - end + @lines = raw.lines.nil? ? nil : map_raw_lines(raw.lines) end def resolve_lines(resolution) - section_id = nil - - lines.map do |line| - unless line.type - section_id = nil - next line - end - - section_id ||= line_code(line) - - case resolution[section_id] - when 'head' - next unless line.type == 'new' - when 'origin' - next unless line.type == 'old' - else - raise MissingResolution, "Missing resolution for section ID: #{section_id}" - end - - line - end.compact - end - - def resolve_content(resolution) - if resolution == content - raise MissingResolution, "Resolved content has no changes for file #{our_path}" - end - - resolution + map_raw_lines(raw.resolve_lines(resolution)) end def highlight_lines! @@ -163,7 +110,7 @@ module Gitlab end def line_code(line) - Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos) + Gitlab::Git.diff_line_code(our_path, line.new_pos, line.old_pos) end def create_match_line(line) @@ -227,15 +174,14 @@ module Gitlab new_path: our_path) end - # Don't try to print merge_request or repository. - def inspect - instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable| - value = instance_variable_get("@#{instance_variable}") + private - "#{instance_variable}=\"#{value}\"" + def map_raw_lines(raw_lines) + raw_lines.map do |raw_line| + Gitlab::Diff::Line.new(raw_line[:full_line], raw_line[:type], + raw_line[:line_obj_index], raw_line[:line_old], + raw_line[:line_new], parent_file: self) end - - "#<#{self.class} #{instance_variables.join(' ')}>" end end end diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index 90f83e0f810..fb28e80ff73 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -1,48 +1,29 @@ module Gitlab module Conflict class FileCollection - ConflictSideMissing = Class.new(StandardError) - - attr_reader :merge_request, :our_commit, :their_commit, :project - - delegate :repository, to: :project - - class << self - # We can only write when getting the merge index from the source - # project, because we will write to that project. We don't use this all - # the time because this fetches a ref into the source project, which - # isn't needed for reading. - def for_resolution(merge_request) - project = merge_request.source_project - - new(merge_request, project).tap do |file_collection| - project - .repository - .with_repo_branch_commit(merge_request.target_project.repository.raw_repository, merge_request.target_branch) do - - yield file_collection - end - end - end - - # We don't need to do `with_repo_branch_commit` here, because the target - # project always fetches source refs when creating merge request diffs. - def read_only(merge_request) - new(merge_request, merge_request.target_project) - end + attr_reader :merge_request, :resolver + + def initialize(merge_request) + source_repo = merge_request.source_project.repository.raw + our_commit = merge_request.source_branch_head.raw + their_commit = merge_request.target_branch_head.raw + target_repo = merge_request.target_project.repository.raw + @resolver = Gitlab::Git::Conflict::Resolver.new(source_repo, our_commit, target_repo, their_commit) + @merge_request = merge_request end - def merge_index - @merge_index ||= repository.rugged.merge_commits(our_commit, their_commit) + def resolve(user, commit_message, files) + args = { + source_branch: merge_request.source_branch, + target_branch: merge_request.target_branch, + commit_message: commit_message || default_commit_message + } + resolver.resolve_conflicts(user, files, args) end def files - @files ||= merge_index.conflicts.map do |conflict| - raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours] - - Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]), - conflict, - merge_request: merge_request) + @files ||= resolver.conflicts.map do |conflict_file| + Gitlab::Conflict::File.new(conflict_file, merge_request: merge_request) end end @@ -61,8 +42,8 @@ module Gitlab end def default_commit_message - conflict_filenames = merge_index.conflicts.map do |conflict| - "# #{conflict[:ours][:path]}" + conflict_filenames = files.map do |conflict| + "# #{conflict.our_path}" end <<EOM.chomp @@ -72,15 +53,6 @@ Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branc #{conflict_filenames.join("\n")} EOM end - - private - - def initialize(merge_request, project) - @merge_request = merge_request - @our_commit = merge_request.source_branch_head.raw.rugged_commit - @their_commit = merge_request.target_branch_head.raw.rugged_commit - @project = project - end end end end diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb deleted file mode 100644 index e3678c914db..00000000000 --- a/lib/gitlab/conflict/parser.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Gitlab - module Conflict - class Parser - UnresolvableError = Class.new(StandardError) - UnmergeableFile = Class.new(UnresolvableError) - UnsupportedEncoding = Class.new(UnresolvableError) - - # Recoverable errors - the conflict can be resolved in an editor, but not with - # sections. - ParserError = Class.new(StandardError) - UnexpectedDelimiter = Class.new(ParserError) - MissingEndDelimiter = Class.new(ParserError) - - def parse(text, our_path:, their_path:, parent_file: nil) - validate_text!(text) - - line_obj_index = 0 - line_old = 1 - line_new = 1 - type = nil - lines = [] - conflict_start = "<<<<<<< #{our_path}" - conflict_middle = '=======' - conflict_end = ">>>>>>> #{their_path}" - - text.each_line.map do |line| - full_line = line.delete("\n") - - if full_line == conflict_start - validate_delimiter!(type.nil?) - - type = 'new' - elsif full_line == conflict_middle - validate_delimiter!(type == 'new') - - type = 'old' - elsif full_line == conflict_end - validate_delimiter!(type == 'old') - - type = nil - elsif line[0] == '\\' - type = 'nonewline' - lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file) - else - lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file) - line_old += 1 if type != 'new' - line_new += 1 if type != 'old' - - line_obj_index += 1 - end - end - - raise MissingEndDelimiter unless type.nil? - - lines - end - - private - - def validate_text!(text) - raise UnmergeableFile if text.blank? # Typically a binary file - raise UnmergeableFile if text.length > 200.kilobytes - - text.force_encoding('UTF-8') - - raise UnsupportedEncoding unless text.valid_encoding? - end - - def validate_delimiter!(condition) - raise UnexpectedDelimiter unless condition - end - end - end -end diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb deleted file mode 100644 index 0b61256b35a..00000000000 --- a/lib/gitlab/conflict/resolution_error.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Gitlab - module Conflict - ResolutionError = Class.new(StandardError) - end -end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 599c3c5deab..ea5891a028a 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -49,7 +49,7 @@ module Gitlab def line_code(line) return if line.meta? - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos) end def line_for_line_code(code) diff --git a/lib/gitlab/diff/line_code.rb b/lib/gitlab/diff/line_code.rb deleted file mode 100644 index f3578ab3d35..00000000000 --- a/lib/gitlab/diff/line_code.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module Diff - class LineCode - def self.generate(file_path, new_line_position, old_line_position) - "#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}" - end - end - end -end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 742f989c50b..7dc9cc7c281 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -17,7 +17,9 @@ module Gitlab # without having to instantiate all the others that come after it. Enumerator.new do |yielder| @lines.each do |line| - next if filename?(line) + # We're expecting a filename parameter only in a meta-part of the diff content + # when type is defined then we're already in a content-part + next if filename?(line) && type.nil? full_line = line.delete("\n") diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index c78fe63f9b5..1f31cdbc96d 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -66,6 +66,10 @@ module Gitlab end end end + + def diff_line_code(file_path, new_line_position, old_line_position) + "#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}" + end end end end diff --git a/lib/gitlab/git/conflict/file.rb b/lib/gitlab/git/conflict/file.rb new file mode 100644 index 00000000000..fc1595f1faf --- /dev/null +++ b/lib/gitlab/git/conflict/file.rb @@ -0,0 +1,86 @@ +module Gitlab + module Git + module Conflict + class File + attr_reader :content, :their_path, :our_path, :our_mode, :repository + + def initialize(repository, commit_oid, conflict, content) + @repository = repository + @commit_oid = commit_oid + @their_path = conflict[:theirs][:path] + @our_path = conflict[:ours][:path] + @our_mode = conflict[:ours][:mode] + @content = content + end + + def lines + return @lines if defined?(@lines) + + begin + @type = 'text' + @lines = Gitlab::Git::Conflict::Parser.parse(content, + our_path: our_path, + their_path: their_path) + rescue Gitlab::Git::Conflict::Parser::ParserError + @type = 'text-editor' + @lines = nil + end + end + + def type + lines unless @type + + @type.inquiry + end + + def our_blob + # REFACTOR NOTE: the source of `commit_oid` used to be + # `merge_request.diff_refs.head_sha`. Instead of passing this value + # around the new lib structure, I decided to use `@commit_oid` which is + # equivalent to `merge_request.source_branch_head.raw.rugged_commit.oid`. + # That is what `merge_request.diff_refs.head_sha` is equivalent to when + # `merge_request` is not persisted (see `MergeRequest#diff_head_commit`). + # I think using the same oid is more consistent anyways, but if Conflicts + # start breaking, the change described above is a good place to look at. + @our_blob ||= repository.blob_at(@commit_oid, our_path) + end + + def line_code(line) + Gitlab::Git.diff_line_code(our_path, line[:line_new], line[:line_old]) + end + + def resolve_lines(resolution) + section_id = nil + + lines.map do |line| + unless line[:type] + section_id = nil + next line + end + + section_id ||= line_code(line) + + case resolution[section_id] + when 'head' + next unless line[:type] == 'new' + when 'origin' + next unless line[:type] == 'old' + else + raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Missing resolution for section ID: #{section_id}" + end + + line + end.compact + end + + def resolve_content(resolution) + if resolution == content + raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Resolved content has no changes for file #{our_path}" + end + + resolution + end + end + end + end +end diff --git a/lib/gitlab/git/conflict/parser.rb b/lib/gitlab/git/conflict/parser.rb new file mode 100644 index 00000000000..3effa9d2d31 --- /dev/null +++ b/lib/gitlab/git/conflict/parser.rb @@ -0,0 +1,91 @@ +module Gitlab + module Git + module Conflict + class Parser + UnresolvableError = Class.new(StandardError) + UnmergeableFile = Class.new(UnresolvableError) + UnsupportedEncoding = Class.new(UnresolvableError) + + # Recoverable errors - the conflict can be resolved in an editor, but not with + # sections. + ParserError = Class.new(StandardError) + UnexpectedDelimiter = Class.new(ParserError) + MissingEndDelimiter = Class.new(ParserError) + + class << self + def parse(text, our_path:, their_path:, parent_file: nil) + validate_text!(text) + + line_obj_index = 0 + line_old = 1 + line_new = 1 + type = nil + lines = [] + conflict_start = "<<<<<<< #{our_path}" + conflict_middle = '=======' + conflict_end = ">>>>>>> #{their_path}" + + text.each_line.map do |line| + full_line = line.delete("\n") + + if full_line == conflict_start + validate_delimiter!(type.nil?) + + type = 'new' + elsif full_line == conflict_middle + validate_delimiter!(type == 'new') + + type = 'old' + elsif full_line == conflict_end + validate_delimiter!(type == 'old') + + type = nil + elsif line[0] == '\\' + type = 'nonewline' + lines << { + full_line: full_line, + type: type, + line_obj_index: line_obj_index, + line_old: line_old, + line_new: line_new + } + else + lines << { + full_line: full_line, + type: type, + line_obj_index: line_obj_index, + line_old: line_old, + line_new: line_new + } + + line_old += 1 if type != 'new' + line_new += 1 if type != 'old' + + line_obj_index += 1 + end + end + + raise MissingEndDelimiter unless type.nil? + + lines + end + + private + + def validate_text!(text) + raise UnmergeableFile if text.blank? # Typically a binary file + raise UnmergeableFile if text.length > 200.kilobytes + + text.force_encoding('UTF-8') + + raise UnsupportedEncoding unless text.valid_encoding? + end + + def validate_delimiter!(condition) + raise UnexpectedDelimiter unless condition + end + end + end + end + end +end diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb new file mode 100644 index 00000000000..df509c5f4ce --- /dev/null +++ b/lib/gitlab/git/conflict/resolver.rb @@ -0,0 +1,91 @@ +module Gitlab + module Git + module Conflict + class Resolver + ConflictSideMissing = Class.new(StandardError) + ResolutionError = Class.new(StandardError) + + def initialize(repository, our_commit, target_repository, their_commit) + @repository = repository + @our_commit = our_commit.rugged_commit + @target_repository = target_repository + @their_commit = their_commit.rugged_commit + end + + def conflicts + @conflicts ||= begin + target_index = @target_repository.rugged.merge_commits(@our_commit, @their_commit) + + # We don't need to do `with_repo_branch_commit` here, because the target + # project always fetches source refs when creating merge request diffs. + target_index.conflicts.map do |conflict| + raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours] + + Gitlab::Git::Conflict::File.new( + @target_repository, + @our_commit.oid, + conflict, + target_index.merge_file(conflict[:ours][:path])[:data] + ) + end + end + end + + def resolve_conflicts(user, files, source_branch:, target_branch:, commit_message:) + @repository.with_repo_branch_commit(@target_repository, target_branch) do + files.each do |file_params| + conflict_file = conflict_for_path(file_params[:old_path], file_params[:new_path]) + + write_resolved_file_to_index(conflict_file, file_params) + end + + unless index.conflicts.empty? + missing_files = index.conflicts.map { |file| file[:ours][:path] } + + raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}" + end + + commit_params = { + message: commit_message, + parents: [@our_commit, @their_commit].map(&:oid) + } + + @repository.commit_index(user, source_branch, index, commit_params) + end + end + + def conflict_for_path(old_path, new_path) + conflicts.find do |conflict| + conflict.their_path == old_path && conflict.our_path == new_path + end + end + + private + + # We can only write when getting the merge index from the source + # project, because we will write to that project. We don't use this all + # the time because this fetches a ref into the source project, which + # isn't needed for reading. + def index + @index ||= @repository.rugged.merge_commits(@our_commit, @their_commit) + end + + def write_resolved_file_to_index(file, params) + if params[:sections] + resolved_lines = file.resolve_lines(params[:sections]) + new_file = resolved_lines.map { |line| line[:full_line] }.join("\n") + + new_file << "\n" if file.our_blob.data.ends_with?("\n") + elsif params[:content] + new_file = file.resolve_content(params[:content]) + end + + our_path = file.our_path + + index.add(path: our_path, oid: @repository.rugged.write(new_file, :blob), mode: file.our_mode) + index.conflict_remove(our_path) + end + end + end + end +end diff --git a/lib/gitlab/git/env.rb b/lib/gitlab/git/env.rb index 80f0731cd99..9d0b47a1a6d 100644 --- a/lib/gitlab/git/env.rb +++ b/lib/gitlab/git/env.rb @@ -30,6 +30,17 @@ module Gitlab RequestStore.fetch(:gitlab_git_env) { {} } end + def self.to_env_hash + env = {} + + all.compact.each do |key, value| + value = value.join(File::PATH_SEPARATOR) if value.is_a?(Array) + env[key.to_s] = value + end + + env + end + def self.[](key) all[key] end diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index d835dcca8ba..ab94ba8a73a 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -3,9 +3,17 @@ module Gitlab class OperationService include Gitlab::Git::Popen - WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do + BranchUpdate = Struct.new(:newrev, :repo_created, :branch_created) do alias_method :repo_created?, :repo_created alias_method :branch_created?, :branch_created + + def self.from_gitaly(branch_update) + new( + branch_update.commit_id, + branch_update.repo_created, + branch_update.branch_created + ) + end end attr_reader :user, :repository @@ -112,7 +120,7 @@ module Gitlab ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name update_ref_in_hooks(ref, newrev, oldrev) - WithBranchResult.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)) + BranchUpdate.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)) end def find_oldrev_from_branch(newrev, branch) diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb index 3d2fc471d28..b45da6020ee 100644 --- a/lib/gitlab/git/popen.rb +++ b/lib/gitlab/git/popen.rb @@ -5,6 +5,8 @@ require 'open3' module Gitlab module Git module Popen + FAST_GIT_PROCESS_TIMEOUT = 15.seconds + def popen(cmd, path, vars = {}) unless cmd.is_a?(Array) raise "System commands must be given as an array of strings" @@ -27,6 +29,67 @@ module Gitlab [@cmd_output, @cmd_status] end + + def popen_with_timeout(cmd, timeout, path, vars = {}) + unless cmd.is_a?(Array) + raise "System commands must be given as an array of strings" + end + + path ||= Dir.pwd + vars['PWD'] = path + + unless File.directory?(path) + FileUtils.mkdir_p(path) + end + + rout, wout = IO.pipe + rerr, werr = IO.pipe + + pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true) + + begin + status = process_wait_with_timeout(pid, timeout) + + # close write ends so we could read them + wout.close + werr.close + + cmd_output = rout.readlines.join + cmd_output << rerr.readlines.join # Copying the behaviour of `popen` which merges stderr into output + + [cmd_output, status.exitstatus] + rescue Timeout::Error => e + kill_process_group_for_pid(pid) + + raise e + ensure + wout.close unless wout.closed? + werr.close unless werr.closed? + + rout.close + rerr.close + end + end + + def process_wait_with_timeout(pid, timeout) + deadline = timeout.seconds.from_now + wait_time = 0.01 + + while deadline > Time.now + sleep(wait_time) + _, status = Process.wait2(pid, Process::WNOHANG) + + return status unless status.nil? + end + + raise Timeout::Error, "Timeout waiting for process ##{pid}" + end + + def kill_process_group_for_pid(pid) + Process.kill("KILL", -pid) + Process.wait(pid) + rescue Errno::ESRCH + end end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index a6b2d189f18..59a54b48ed9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -704,7 +704,17 @@ module Gitlab tags.find { |tag| tag.name == name } end - def merge(user, source_sha, target_branch, message) + def merge(user, source_sha, target_branch, message, &block) + gitaly_migrate(:operation_user_merge_branch) do |is_enabled| + if is_enabled + gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block) + else + rugged_merge(user, source_sha, target_branch, message, &block) + end + end + end + + def rugged_merge(user, source_sha, target_branch, message) committer = Gitlab::Git.committer_hash(email: user.email, name: user.name) OperationService.new(user, self).with_branch(target_branch) do |start_commit| @@ -1062,6 +1072,13 @@ module Gitlab end # Refactoring aid; allows us to copy code from app/models/repository.rb + def run_git_with_timeout(args, timeout, env: {}) + circuit_breaker.perform do + popen_with_timeout([Gitlab.config.git.bin_path, *args], timeout, path, env) + end + end + + # Refactoring aid; allows us to copy code from app/models/repository.rb def commit(ref = 'HEAD') Gitlab::Git::Commit.find(self, ref) end @@ -1092,6 +1109,24 @@ module Gitlab popen(args, @path).last.zero? end + def blob_at(sha, path) + Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha) + end + + def commit_index(user, branch_name, index, options) + committer = user_to_committer(user) + + OperationService.new(user, self).with_branch(branch_name) do + commit_params = options.merge( + tree: index.write_tree(rugged), + author: committer, + committer: committer + ) + + create_commit(commit_params) + end + end + def gitaly_repository Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository) end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index 92a6a672534..60b2a4ec411 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -28,7 +28,7 @@ module Gitlab private def execute(args) - output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys) + output, status = popen(args, nil, Gitlab::Git::Env.to_env_hash) unless status.zero? raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}" diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index cf36106e23d..6c1ae19ff11 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -70,15 +70,27 @@ module Gitlab # All Gitaly RPC call sites should use GitalyClient.call. This method # makes sure that per-request authentication headers are set. + # + # This method optionally takes a block which receives the keyword + # arguments hash 'kwargs' that will be passed to gRPC. This allows the + # caller to modify or augment the keyword arguments. The block must + # return a hash. + # + # For example: + # + # GitalyClient.call(storage, service, rpc, request) do |kwargs| + # kwargs.merge(deadline: Time.now + 10) + # end + # def self.call(storage, service, rpc, request) enforce_gitaly_request_limits(:call) - metadata = request_metadata(storage) - metadata = yield(metadata) if block_given? - stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend + kwargs = request_kwargs(storage) + kwargs = yield(kwargs) if block_given? + stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend end - def self.request_metadata(storage) + def self.request_kwargs(storage) encoded_token = Base64.strict_encode64(token(storage).to_s) metadata = { 'authorization' => "Bearer #{encoded_token}", diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 81ddaf13e10..91f34011f6e 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -74,6 +74,37 @@ module Gitlab raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error end end + + def user_merge_branch(user, source_sha, target_branch, message) + request_enum = QueueEnumerator.new + response_enum = GitalyClient.call( + @repository.storage, + :operation_service, + :user_merge_branch, + request_enum.each + ) + + request_enum.push( + Gitaly::UserMergeBranchRequest.new( + repository: @gitaly_repo, + user: Util.gitaly_user(user), + commit_id: source_sha, + branch: GitalyClient.encode(target_branch), + message: GitalyClient.encode(message) + ) + ) + + yield response_enum.next.commit_id + + request_enum.push(Gitaly::UserMergeBranchRequest.new(apply: true)) + + branch_update = response_enum.next.branch_update + raise Gitlab::Git::CommitError.new('failed to apply merge to branch') unless branch_update.commit_id.present? + + Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update) + ensure + request_enum.close + end end end end diff --git a/lib/gitlab/gitaly_client/queue_enumerator.rb b/lib/gitlab/gitaly_client/queue_enumerator.rb new file mode 100644 index 00000000000..b8018029552 --- /dev/null +++ b/lib/gitlab/gitaly_client/queue_enumerator.rb @@ -0,0 +1,28 @@ +module Gitlab + module GitalyClient + class QueueEnumerator + def initialize + @queue = Queue.new + end + + def push(elem) + @queue << elem + end + + def close + push(:close) + end + + def each + return enum_for(:each) unless block_given? + + loop do + elem = @queue.pop + break if elem == :close + + yield elem + end + end + end + end +end diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb index e21922070c1..8911b81ec9a 100644 --- a/lib/gitlab/github_import/comment_formatter.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -38,7 +38,7 @@ module Gitlab end def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos) end def on_diff? diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 7c02c9c5c48..894bd5efae5 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -26,7 +26,6 @@ module Gitlab apple-touch-icon.png assets autocomplete - boards ci dashboard deploy.html diff --git a/lib/gitlab/quick_actions/spend_time_and_date_separator.rb b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb new file mode 100644 index 00000000000..3f52402b31f --- /dev/null +++ b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb @@ -0,0 +1,54 @@ +module Gitlab + module QuickActions + # This class takes spend command argument + # and separates date and time from spend command arguments if it present + # example: + # spend_command_time_and_date = "15m 2017-01-02" + # SpendTimeAndDateSeparator.new(spend_command_time_and_date).execute + # => [900, Mon, 02 Jan 2017] + # if date doesn't present return time with current date + # in other cases return nil + class SpendTimeAndDateSeparator + DATE_REGEX = /(\d{2,4}[\/\-.]\d{1,2}[\/\-.]\d{1,2})/ + + def initialize(spend_command_arg) + @spend_arg = spend_command_arg + end + + def execute + return if @spend_arg.blank? + return [get_time, DateTime.now.to_date] unless date_present? + return unless valid_date? + + [get_time, get_date] + end + + private + + def get_time + raw_time = @spend_arg.gsub(DATE_REGEX, '') + Gitlab::TimeTrackingFormatter.parse(raw_time) + end + + def get_date + string_date = @spend_arg.match(DATE_REGEX)[0] + Date.parse(string_date) + end + + def date_present? + DATE_REGEX =~ @spend_arg + end + + def valid_date? + string_date = @spend_arg.match(DATE_REGEX)[0] + date = Date.parse(string_date) rescue nil + + date_past_or_today?(date) + end + + def date_past_or_today?(date) + date&.past? || date&.today? + end + end + end +end diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb index a0a2769cf9e..a1f689d94d9 100644 --- a/lib/gitlab/sidekiq_status.rb +++ b/lib/gitlab/sidekiq_status.rb @@ -51,6 +51,13 @@ module Gitlab self.num_running(job_ids).zero? end + # Returns true if the given job is running + # + # job_id - The Sidekiq job ID to check. + def self.running?(job_id) + num_running([job_id]) > 0 + end + # Returns the number of jobs that are running. # # job_ids - The Sidekiq job IDs to check. diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 3f3ba77d47f..70a403652e7 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -49,6 +49,8 @@ module Gitlab deployments: Deployment.count, environments: ::Environment.count, gcp_clusters: ::Gcp::Cluster.count, + gcp_clusters_enabled: ::Gcp::Cluster.enabled.count, + gcp_clusters_disabled: ::Gcp::Cluster.disabled.count, in_review_folder: ::Environment.in_review_folder.count, groups: Group.count, issues: Issue.count, diff --git a/package.json b/package.json index 5aa3ce3f757..620b39766f4 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "jszip": "^3.1.3", "jszip-utils": "^0.0.2", "marked": "^0.3.6", - "monaco-editor": "0.8.3", + "monaco-editor": "0.10.0", "mousetrap": "^1.4.6", "name-all-modules-plugin": "^1.0.1", "pikaday": "^1.5.1", @@ -66,7 +66,7 @@ "vue-loader": "^11.3.4", "vue-resource": "^1.3.4", "vue-template-compiler": "^2.2.6", - "vuex": "^2.3.1", + "vuex": "^3.0.0", "webpack": "^3.5.5", "webpack-bundle-analyzer": "^2.8.2", "webpack-stats-plugin": "^0.1.5" @@ -18,6 +18,7 @@ module QA # Support files # autoload :Actable, 'qa/scenario/actable' + autoload :Entrypoint, 'qa/scenario/entrypoint' autoload :Template, 'qa/scenario/template' ## @@ -25,6 +26,10 @@ module QA # module Test autoload :Instance, 'qa/scenario/test/instance' + + module Integration + autoload :Mattermost, 'qa/scenario/test/integration/mattermost' + end end ## diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb new file mode 100644 index 00000000000..33cb2696f8f --- /dev/null +++ b/qa/qa/scenario/entrypoint.rb @@ -0,0 +1,36 @@ +module QA + module Scenario + ## + # Base class for running the suite against any GitLab instance, + # including staging and on-premises installation. + # + class Entrypoint < Template + def self.tags(*tags) + @tags = tags + end + + def self.get_tags + @tags + end + + def perform(address, *files) + Specs::Config.perform do |specs| + specs.address = address + end + + ## + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks + + Specs::Runner.perform do |specs| + specs.rspec( + tty: true, + tags: self.class.get_tags, + files: files.any? ? files : 'qa/specs/features' + ) + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index 689292bc60b..e2a1f6bf2bd 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -5,21 +5,8 @@ module QA # Run test suite against any GitLab instance, # including staging and on-premises installation. # - class Instance < Scenario::Template - def perform(address, *files) - Specs::Config.perform do |specs| - specs.address = address - end - - ## - # Perform before hooks, which are different for CE and EE - # - Runtime::Release.perform_before_hooks - - Specs::Runner.perform do |specs| - specs.rspec('--tty', files.any? ? files : 'qa/specs/features') - end - end + class Instance < Entrypoint + tags :core end end end diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb new file mode 100644 index 00000000000..4732f2b635b --- /dev/null +++ b/qa/qa/scenario/test/integration/mattermost.rb @@ -0,0 +1,15 @@ +module QA + module Scenario + module Test + module Integration + ## + # Run test suite against any GitLab instance where mattermost is enabled, + # including staging and on-premises installation. + # + class Mattermost < Scenario::Entrypoint + tags :core, :mattermost + end + end + end + end +end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb index 8e1ae6efa47..ba19ce17ee5 100644 --- a/qa/qa/specs/features/login/standard_spec.rb +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'standard root login' do + feature 'standard root login', :core do scenario 'user logs in using credentials' do Page::Main::Entry.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb new file mode 100644 index 00000000000..c4afd83c8e4 --- /dev/null +++ b/qa/qa/specs/features/mattermost/group_create_spec.rb @@ -0,0 +1,16 @@ +module QA + feature 'create a new group', :mattermost do + scenario 'creating a group with a mattermost team' do + Page::Main::Entry.act { sign_in_using_credentials } + Page::Main::Menu.act { go_to_groups } + + Page::Dashboard::Groups.perform do |page| + page.go_to_new_group + + expect(page).to have_content( + /Create a Mattermost team for this group/ + ) + end + end + end +end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index 610492b9717..27eb22f15a6 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'create a new project' do + feature 'create a new project', :core do scenario 'user creates a new project' do Page::Main::Entry.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index 521bd955857..3571173783d 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -1,5 +1,5 @@ module QA - feature 'clone code from the repository' do + feature 'clone code from the repository', :core do context 'with regular account over http' do given(:location) do Page::Project::Show.act do diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 5fe45d63d37..0e691fb0d75 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -1,7 +1,7 @@ module QA - feature 'push code to repository' do + feature 'push code to repository', :core do context 'with regular account over http' do - scenario 'user pushes code to the repository' do + scenario 'user pushes code to the repository' do Page::Main::Entry.act { sign_in_using_credentials } Scenario::Gitlab::Project::Create.perform do |scenario| diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index 83ae15d0995..2aa18d5d3a1 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -5,7 +5,14 @@ module QA class Runner include Scenario::Actable - def rspec(*args) + def rspec(tty: false, tags: [], files: ['qa/specs/features']) + args = [] + args << '--tty' if tty + tags.to_a.each do |tag| + args << ['-t', tag.to_s] + end + args << files + RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| abort if status.nonzero? end diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 6d8b9865dcb..fc1bf67d7b9 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -84,7 +84,7 @@ describe 'bin/changelog' do expect do expect do expect { described_class.read_type }.to raise_error(SystemExit) - end.to output("Invalid category index, please select an index between 1 and 7\n").to_stderr + end.to output("Invalid category index, please select an index between 1 and 8\n").to_stderr end.to output.to_stdout end end diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb index 393d38c6e6b..c6d50c28106 100644 --- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb @@ -17,8 +17,8 @@ describe Projects::MergeRequests::ConflictsController do describe 'GET show' do context 'when the conflicts cannot be resolved in the UI' do before do - allow_any_instance_of(Gitlab::Conflict::Parser) - .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) + allow(Gitlab::Git::Conflict::Parser).to receive(:parse) + .and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile) get :show, namespace_id: merge_request_with_conflicts.project.namespace.to_param, @@ -109,8 +109,8 @@ describe Projects::MergeRequests::ConflictsController do context 'when the conflicts cannot be resolved in the UI' do before do - allow_any_instance_of(Gitlab::Conflict::Parser) - .to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) + allow(Gitlab::Git::Conflict::Parser).to receive(:parse) + .and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile) conflict_for_path('files/ruby/regex.rb') end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index c4817eb947b..4965f803883 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe 'Issue Boards', :js do + include BoardHelpers + let(:user) { create(:user) } let(:user2) { create(:user) } let(:project) { create(:project, :public) } @@ -309,6 +311,21 @@ describe 'Issue Boards', :js do expect(card).to have_selector('.label', count: 1) expect(card).not_to have_content(stretch.title) end + + it 'creates new label' do + click_card(card) + + page.within('.labels') do + click_link 'Edit' + click_link 'Create new label' + fill_in 'new_label_name', with: 'test label' + first('.suggest-colors-dropdown a').click + click_button 'Create' + wait_for_requests + + expect(page).to have_link 'test label' + end + end end context 'subscription' do @@ -322,19 +339,4 @@ describe 'Issue Boards', :js do end end end - - def click_card(card) - page.within(card) do - first('.card-number').click - end - - wait_for_sidebar - end - - def wait_for_sidebar - # loop until the CSS transition is complete - Timeout.timeout(0.5) do - loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290 - end - end end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 78c350c8ee4..d63cbe578d8 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -14,7 +14,6 @@ feature 'Projects > Wiki > User previews markdown changes', :js do background do project.team << [user, :master] - WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute sign_in(user) diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb index eaff5f876b6..f70d1e710dd 100644 --- a/spec/features/projects/wiki/shortcuts_spec.rb +++ b/spec/features/projects/wiki/shortcuts_spec.rb @@ -3,9 +3,7 @@ require 'spec_helper' feature 'Wiki shortcuts', :js do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } - let(:wiki_page) do - WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute - end + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) } before do sign_in(user) diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb index 9a92622ba2b..37a118c34ab 100644 --- a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb @@ -3,14 +3,7 @@ require 'spec_helper' describe 'Projects > Wiki > User views Git access wiki page' do let(:user) { create(:user) } let(:project) { create(:project, :public) } - let(:wiki_page) do - WikiPages::CreateService.new( - project, - user, - title: 'home', - content: '[some link](other-page)' - ).execute - end + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) } before do sign_in(user) diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb index cf9fe4c1ad1..ebb3bd044c1 100644 --- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -18,12 +18,7 @@ describe 'Projects > Wiki > User views wiki in project page' do context 'when wiki homepage contains a link' do before do - WikiPages::CreateService.new( - project, - user, - title: 'home', - content: '[some link](other-page)' - ).execute + create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) end it 'displays the correct URL for the link' do diff --git a/spec/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz Binary files differindex d0e89378b3e..5c4ea9690e8 100644 --- a/spec/fixtures/pages.tar.gz +++ b/spec/fixtures/pages.tar.gz diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip Binary files differindex 9558fcd4b94..9bb75f953f8 100644 --- a/spec/fixtures/pages.zip +++ b/spec/fixtures/pages.zip diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index 060ffaa339b..b669aabcee4 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -2,6 +2,7 @@ import flash, { createFlashEl, createAction, hideFlash, + removeFlashClickListener, } from '~/flash'; describe('Flash', () => { @@ -266,4 +267,24 @@ describe('Flash', () => { }); }); }); + + describe('removeFlashClickListener', () => { + beforeEach(() => { + document.body.innerHTML += '<div class="flash-container"><div class="flash"></div></div>'; + }); + + it('removes global flash on click', (done) => { + const flashEl = document.querySelector('.flash'); + + removeFlashClickListener(flashEl, false); + + flashEl.parentNode.click(); + + setTimeout(() => { + expect(document.querySelector('.flash')).toBeNull(); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/helpers/set_timeout_promise_helper.js b/spec/javascripts/helpers/set_timeout_promise_helper.js new file mode 100644 index 00000000000..1478073413c --- /dev/null +++ b/spec/javascripts/helpers/set_timeout_promise_helper.js @@ -0,0 +1,3 @@ +export default (time = 0) => new Promise((resolve) => { + setTimeout(resolve, time); +}); diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index c7179b3e03d..4a210faa017 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -30,7 +30,6 @@ describe('Job details header', () => { email: 'foo@bar.com', avatar_url: 'link', }, - retry_path: 'path', new_issue_path: 'path', }, isLoading: false, @@ -49,12 +48,6 @@ describe('Job details header', () => { ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); }); - it('should render retry link', () => { - expect( - vm.$el.querySelector('.js-retry-button').getAttribute('href'), - ).toEqual(props.job.retry_path); - }); - it('should render new issue link', () => { expect( vm.$el.querySelector('.js-new-issue').getAttribute('href'), diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index e604dcc152d..0635de4b30b 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -2,29 +2,13 @@ import Vue from 'vue'; import repoCommitSection from '~/repo/components/repo_commit_section.vue'; import RepoStore from '~/repo/stores/repo_store'; import RepoService from '~/repo/services/repo_service'; +import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; describe('RepoCommitSection', () => { const branch = 'master'; const projectUrl = 'projectUrl'; - const changedFiles = [{ - id: 0, - changed: true, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`, - path: 'dir/file0.ext', - newContent: 'a', - }, { - id: 1, - changed: true, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`, - path: 'dir/file1.ext', - newContent: 'b', - }]; - const openedFiles = changedFiles.concat([{ - id: 2, - url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`, - path: 'dir/file2.ext', - changed: false, - }]); + let changedFiles; + let openedFiles; RepoStore.projectUrl = projectUrl; @@ -34,6 +18,29 @@ describe('RepoCommitSection', () => { return new RepoCommitSection().$mount(el); } + beforeEach(() => { + // Create a copy for each test because these can get modified directly + changedFiles = [{ + id: 0, + changed: true, + url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`, + path: 'dir/file0.ext', + newContent: 'a', + }, { + id: 1, + changed: true, + url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`, + path: 'dir/file1.ext', + newContent: 'b', + }]; + openedFiles = changedFiles.concat([{ + id: 2, + url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`, + path: 'dir/file2.ext', + changed: false, + }]); + }); + it('renders a commit section', () => { RepoStore.isCommitable = true; RepoStore.currentBranch = branch; @@ -85,55 +92,104 @@ describe('RepoCommitSection', () => { expect(vm.$el.innerHTML).toBeFalsy(); }); - it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => { + describe('when submitting', () => { + let el; + let vm; const projectId = 'projectId'; const commitMessage = 'commitMessage'; - RepoStore.isCommitable = true; - RepoStore.currentBranch = branch; - RepoStore.targetBranch = branch; - RepoStore.openedFiles = openedFiles; - RepoStore.projectId = projectId; - // We need to append to body to get form `submit` events working - // Otherwise we run into, "Form submission canceled because the form is not connected" - // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm - const el = document.createElement('div'); - document.body.appendChild(el); - - const vm = createComponent(el); - const commitMessageEl = vm.$el.querySelector('#commit-message'); - const submitCommit = vm.$refs.submitCommit; + beforeEach((done) => { + RepoStore.isCommitable = true; + RepoStore.currentBranch = branch; + RepoStore.targetBranch = branch; + RepoStore.openedFiles = openedFiles; + RepoStore.projectId = projectId; + + // We need to append to body to get form `submit` events working + // Otherwise we run into, "Form submission canceled because the form is not connected" + // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm + el = document.createElement('div'); + document.body.appendChild(el); + + vm = createComponent(el); + vm.commitMessage = commitMessage; + + spyOn(vm, 'tryCommit').and.callThrough(); + spyOn(vm, 'redirectToNewMr').and.stub(); + spyOn(vm, 'redirectToBranch').and.stub(); + spyOn(RepoService, 'commitFiles').and.returnValue(Promise.resolve()); + spyOn(RepoService, 'getBranch').and.returnValue(Promise.resolve({ + commit: { + id: 1, + short_id: 1, + }, + })); + + // Wait for the vm data to be in place + Vue.nextTick(() => { + done(); + }); + }); - vm.commitMessage = commitMessage; + afterEach(() => { + vm.$destroy(); + el.remove(); + }); - Vue.nextTick(() => { + it('shows commit message', () => { + const commitMessageEl = vm.$el.querySelector('#commit-message'); expect(commitMessageEl.value).toBe(commitMessage); - expect(submitCommit.disabled).toBeFalsy(); + }); - spyOn(vm, 'makeCommit').and.callThrough(); - spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve()); + it('allows you to submit', () => { + const submitCommit = vm.$refs.submitCommit; + expect(submitCommit.disabled).toBeFalsy(); + }); + it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => { + const submitCommit = vm.$refs.submitCommit; submitCommit.click(); - Vue.nextTick(() => { - expect(vm.makeCommit).toHaveBeenCalled(); - expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy(); - - const args = RepoService.commitFiles.calls.allArgs()[0]; - const { commit_message, actions, branch: payloadBranch } = args[0]; - - expect(commit_message).toBe(commitMessage); - expect(actions.length).toEqual(2); - expect(payloadBranch).toEqual(branch); - expect(actions[0].action).toEqual('update'); - expect(actions[1].action).toEqual('update'); - expect(actions[0].content).toEqual(openedFiles[0].newContent); - expect(actions[1].content).toEqual(openedFiles[1].newContent); - expect(actions[0].file_path).toEqual(openedFiles[0].path); - expect(actions[1].file_path).toEqual(openedFiles[1].path); + // Wait for the branch check to finish + getSetTimeoutPromise() + .then(() => Vue.nextTick()) + .then(() => { + expect(vm.tryCommit).toHaveBeenCalled(); + expect(submitCommit.querySelector('.js-commit-loading-icon')).toBeTruthy(); + expect(vm.redirectToBranch).toHaveBeenCalled(); + + const args = RepoService.commitFiles.calls.allArgs()[0]; + const { commit_message, actions, branch: payloadBranch } = args[0]; + + expect(commit_message).toBe(commitMessage); + expect(actions.length).toEqual(2); + expect(payloadBranch).toEqual(branch); + expect(actions[0].action).toEqual('update'); + expect(actions[1].action).toEqual('update'); + expect(actions[0].content).toEqual(openedFiles[0].newContent); + expect(actions[1].content).toEqual(openedFiles[1].newContent); + expect(actions[0].file_path).toEqual(openedFiles[0].path); + expect(actions[1].file_path).toEqual(openedFiles[1].path); + }) + .then(done) + .catch(done.fail); + }); - done(); - }); + it('redirects to MR creation page if start new MR checkbox checked', (done) => { + vm.startNewMR = true; + + Vue.nextTick() + .then(() => { + const submitCommit = vm.$refs.submitCommit; + submitCommit.click(); + }) + // Wait for the branch check to finish + .then(() => getSetTimeoutPromise()) + .then(() => { + expect(vm.redirectToNewMr).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); }); @@ -143,6 +199,7 @@ describe('RepoCommitSection', () => { const vm = { submitCommitsLoading: true, changedFiles: new Array(10), + openedFiles: new Array(3), commitMessage: 'commitMessage', editMode: true, }; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index a912e150e9b..f6320db8dc4 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,7 +1,5 @@ -/* global ShortcutsIssuable */ - import '~/copy_as_gfm'; -import '~/shortcuts_issuable'; +import ShortcutsIssuable from '~/shortcuts_issuable'; describe('ShortcutsIssuable', () => { const fixtureName = 'merge_requests/diff_comment.html.raw'; diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js index 53e4c68beb3..a2a609edef9 100644 --- a/spec/javascripts/shortcuts_spec.js +++ b/spec/javascripts/shortcuts_spec.js @@ -1,4 +1,5 @@ -/* global Shortcuts */ +import Shortcuts from '~/shortcuts'; + describe('Shortcuts', () => { const fixtureName = 'merge_requests/diff_comment.html.raw'; const createEvent = (type, target) => $.Event(type, { @@ -8,19 +9,17 @@ describe('Shortcuts', () => { preloadFixtures(fixtureName); describe('toggleMarkdownPreview', () => { - let sc; - beforeEach(() => { loadFixtures(fixtureName); spyOnEvent('.js-new-note-form .js-md-preview-button', 'focus'); spyOnEvent('.edit-note .js-md-preview-button', 'focus'); - sc = new Shortcuts(); + new Shortcuts(); // eslint-disable-line no-new }); it('focuses preview button in form', () => { - sc.toggleMarkdownPreview( + Shortcuts.toggleMarkdownPreview( createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text'), )); @@ -31,7 +30,7 @@ describe('Shortcuts', () => { document.querySelector('.js-note-edit').click(); setTimeout(() => { - sc.toggleMarkdownPreview( + Shortcuts.toggleMarkdownPreview( createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text'), )); diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb index a4d7628b03a..5944ce8049a 100644 --- a/spec/lib/gitlab/conflict/file_collection_spec.rb +++ b/spec/lib/gitlab/conflict/file_collection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Conflict::FileCollection do let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') } - let(:file_collection) { described_class.read_only(merge_request) } + let(:file_collection) { described_class.new(merge_request) } describe '#files' do it 'returns an array of Conflict::Files' do diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 5356e9742b4..bf981d2f6f6 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -8,9 +8,10 @@ describe Gitlab::Conflict::File do let(:our_commit) { rugged.branches['conflict-resolvable'].target } let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) } let(:index) { rugged.merge_commits(our_commit, their_commit) } - let(:conflict) { index.conflicts.last } - let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') } - let(:conflict_file) { described_class.new(merge_file_result, conflict, merge_request: merge_request) } + let(:rugged_conflict) { index.conflicts.last } + let(:raw_conflict_content) { index.merge_file('files/ruby/regex.rb')[:data] } + let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit.oid, rugged_conflict, raw_conflict_content) } + let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) } describe '#resolve_lines' do let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact } @@ -48,18 +49,18 @@ describe Gitlab::Conflict::File do end end - it 'raises MissingResolution when passed a hash without resolutions for all sections' do + it 'raises ResolutionError when passed a hash without resolutions for all sections' do empty_hash = section_keys.map { |key| [key, nil] }.to_h invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h expect { conflict_file.resolve_lines({}) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) expect { conflict_file.resolve_lines(empty_hash) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) expect { conflict_file.resolve_lines(invalid_hash) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) end end @@ -144,7 +145,7 @@ describe Gitlab::Conflict::File do end context 'with an example file' do - let(:file) do + let(:raw_conflict_content) do <<FILE # Ensure there is no match line header here def username_regexp @@ -220,7 +221,6 @@ end FILE end - let(:conflict_file) { described_class.new({ data: file }, conflict, merge_request: merge_request) } let(:sections) { conflict_file.sections } it 'sets the correct match line headers' do diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index 8af49ed50ff..80c8c189665 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -143,4 +143,21 @@ eos it { expect(parser.parse([])).to eq([]) } it { expect(parser.parse(nil)).to eq([]) } end + + describe 'tolerates special diff markers in a content' do + it "counts lines correctly" do + diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + +ipsum + +++ b + -ipsum + END + + lines = parser.parse(diff.lines).to_a + + expect(lines.size).to eq(3) + end + end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 9bf54fdecc4..245f24e96d4 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -108,7 +108,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 15) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 15) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -149,7 +149,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -189,7 +189,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 13, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 13, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -233,7 +233,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 5) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 5) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -274,7 +274,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -314,7 +314,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 4, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 4, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -357,7 +357,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -399,7 +399,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + line_code = Gitlab::Git.diff_line_code(subject.file_path, subject.new_line, 0) expect(subject.line_code(project.repository)).to eq(line_code) end @@ -447,7 +447,7 @@ describe Gitlab::Diff::Position do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + line_code = Gitlab::Git.diff_line_code(subject.file_path, 0, subject.old_line) expect(subject.line_code(project.repository)).to eq(line_code) end diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb index fce606a2bb5..7b035a381f1 100644 --- a/spec/lib/gitlab/conflict/parser_spec.rb +++ b/spec/lib/gitlab/git/conflict/parser_spec.rb @@ -1,11 +1,9 @@ require 'spec_helper' -describe Gitlab::Conflict::Parser do - let(:parser) { described_class.new } - - describe '#parse' do +describe Gitlab::Git::Conflict::Parser do + describe '.parse' do def parse_text(text) - parser.parse(text, our_path: 'README.md', their_path: 'README.md') + described_class.parse(text, our_path: 'README.md', their_path: 'README.md') end context 'when the file has valid conflicts' do @@ -87,33 +85,37 @@ CONFLICT end let(:lines) do - parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb') + described_class.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb') + end + let(:old_line_numbers) do + lines.select { |line| line[:type] != 'new' }.map { |line| line[:line_old] } end + let(:new_line_numbers) do + lines.select { |line| line[:type] != 'old' }.map { |line| line[:line_new] } + end + let(:line_indexes) { lines.map { |line| line[:line_obj_index] } } it 'sets our lines as new lines' do - expect(lines[8..13]).to all(have_attributes(type: 'new')) - expect(lines[26..27]).to all(have_attributes(type: 'new')) - expect(lines[56..57]).to all(have_attributes(type: 'new')) + expect(lines[8..13]).to all(include(type: 'new')) + expect(lines[26..27]).to all(include(type: 'new')) + expect(lines[56..57]).to all(include(type: 'new')) end it 'sets their lines as old lines' do - expect(lines[14..19]).to all(have_attributes(type: 'old')) - expect(lines[28..29]).to all(have_attributes(type: 'old')) - expect(lines[58..59]).to all(have_attributes(type: 'old')) + expect(lines[14..19]).to all(include(type: 'old')) + expect(lines[28..29]).to all(include(type: 'old')) + expect(lines[58..59]).to all(include(type: 'old')) end it 'sets non-conflicted lines as both' do - expect(lines[0..7]).to all(have_attributes(type: nil)) - expect(lines[20..25]).to all(have_attributes(type: nil)) - expect(lines[30..55]).to all(have_attributes(type: nil)) - expect(lines[60..62]).to all(have_attributes(type: nil)) + expect(lines[0..7]).to all(include(type: nil)) + expect(lines[20..25]).to all(include(type: nil)) + expect(lines[30..55]).to all(include(type: nil)) + expect(lines[60..62]).to all(include(type: nil)) end - it 'sets consecutive line numbers for index, old_pos, and new_pos' do - old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos) - new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos) - - expect(lines.map(&:index)).to eq(0.upto(62).to_a) + it 'sets consecutive line numbers for line_obj_index, line_old, and line_new' do + expect(line_indexes).to eq(0.upto(62).to_a) expect(old_line_numbers).to eq(1.upto(53).to_a) expect(new_line_numbers).to eq(1.upto(53).to_a) end @@ -123,12 +125,12 @@ CONFLICT context 'when there is a non-start delimiter first' do it 'raises UnexpectedDelimiter when there is a middle delimiter first' do expect { parse_text('=======') } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when there is an end delimiter first' do expect { parse_text('>>>>>>> README.md') } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when there is an end delimiter for a different path first' do @@ -143,12 +145,12 @@ CONFLICT it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do expect { parse_text(start_text + '>>>>>>> README.md' + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do expect { parse_text(start_text + start_text + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when it is followed by a start delimiter for a different path' do @@ -163,12 +165,12 @@ CONFLICT it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do expect { parse_text(start_text + '=======' + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do expect { parse_text(start_text + start_text + end_text) } - .to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::UnexpectedDelimiter) end it 'does not raise when it is followed by a start delimiter for another path' do @@ -181,25 +183,25 @@ CONFLICT start_text = "<<<<<<< README.md\n=======\n" expect { parse_text(start_text) } - .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter) expect { parse_text(start_text + '>>>>>>> some-other-path.md') } - .to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter) + .to raise_error(Gitlab::Git::Conflict::Parser::MissingEndDelimiter) end end context 'other file types' do it 'raises UnmergeableFile when lines is blank, indicating a binary file' do expect { parse_text('') } - .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile) expect { parse_text(nil) } - .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile) end it 'raises UnmergeableFile when the file is over 200 KB' do expect { parse_text('a' * 204801) } - .to raise_error(Gitlab::Conflict::Parser::UnmergeableFile) + .to raise_error(Gitlab::Git::Conflict::Parser::UnmergeableFile) end # All text from Rugged has an encoding of ASCII_8BIT, so force that in @@ -214,7 +216,7 @@ CONFLICT context 'when the file contains non-UTF-8 characters' do it 'raises UnsupportedEncoding' do expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) } - .to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding) + .to raise_error(Gitlab::Git::Conflict::Parser::UnsupportedEncoding) end end end diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/env_spec.rb index d9df99bfe05..03836d49518 100644 --- a/spec/lib/gitlab/git/env_spec.rb +++ b/spec/lib/gitlab/git/env_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::Env do - describe "#set" do + describe ".set" do context 'with RequestStore.store disabled' do before do allow(RequestStore).to receive(:active?).and_return(false) @@ -34,25 +34,57 @@ describe Gitlab::Git::Env do end end - describe "#all" do + describe ".all" do context 'with RequestStore.store enabled' do before do allow(RequestStore).to receive(:active?).and_return(true) described_class.set( GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar') + GIT_ALTERNATE_OBJECT_DIRECTORIES: ['bar']) end it 'returns an env hash' do expect(described_class.all).to eq({ 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => ['bar'] }) end end end - describe "#[]" do + describe ".to_env_hash" do + context 'with RequestStore.store enabled' do + using RSpec::Parameterized::TableSyntax + + let(:key) { 'GIT_OBJECT_DIRECTORY' } + subject { described_class.to_env_hash } + + where(:input, :output) do + nil | nil + 'foo' | 'foo' + [] | '' + ['foo'] | 'foo' + %w[foo bar] | 'foo:bar' + end + + with_them do + before do + allow(RequestStore).to receive(:active?).and_return(true) + described_class.set(key.to_sym => input) + end + + it 'puts the right value in the hash' do + if output + expect(subject.fetch(key)).to eq(output) + else + expect(subject.has_key?(key)).to eq(false) + end + end + end + end + end + + describe ".[]" do context 'with RequestStore.store enabled' do before do allow(RequestStore).to receive(:active?).and_return(true) diff --git a/spec/lib/gitlab/git/popen_spec.rb b/spec/lib/gitlab/git/popen_spec.rb new file mode 100644 index 00000000000..2b65bc1cf15 --- /dev/null +++ b/spec/lib/gitlab/git/popen_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +describe 'Gitlab::Git::Popen' do + let(:path) { Rails.root.join('tmp').to_s } + + let(:klass) do + Class.new(Object) do + include Gitlab::Git::Popen + end + end + + context 'popen' do + context 'zero status' do + let(:result) { klass.new.popen(%w(ls), path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to be_zero } + it { expect(output).to include('tests') } + end + + context 'non-zero status' do + let(:result) { klass.new.popen(%w(cat NOTHING), path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to eq(1) } + it { expect(output).to include('No such file or directory') } + end + + context 'unsafe string command' do + it 'raises an error when it gets called with a string argument' do + expect { klass.new.popen('ls', path) }.to raise_error(RuntimeError) + end + end + + context 'with custom options' do + let(:vars) { { 'foobar' => 123, 'PWD' => path } } + let(:options) { { chdir: path } } + + it 'calls popen3 with the provided environment variables' do + expect(Open3).to receive(:popen3).with(vars, 'ls', options) + + klass.new.popen(%w(ls), path, { 'foobar' => 123 }) + end + end + + context 'use stdin' do + let(:result) { klass.new.popen(%w[cat], path) { |stdin| stdin.write 'hello' } } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to be_zero } + it { expect(output).to eq('hello') } + end + end + + context 'popen_with_timeout' do + let(:timeout) { 1.second } + + context 'no timeout' do + context 'zero status' do + let(:result) { klass.new.popen_with_timeout(%w(ls), timeout, path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to be_zero } + it { expect(output).to include('tests') } + end + + context 'non-zero status' do + let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) } + let(:output) { result.first } + let(:status) { result.last } + + it { expect(status).to eq(1) } + it { expect(output).to include('No such file or directory') } + end + + context 'unsafe string command' do + it 'raises an error when it gets called with a string argument' do + expect { klass.new.popen_with_timeout('ls', timeout, path) }.to raise_error(RuntimeError) + end + end + end + + context 'timeout' do + context 'timeout' do + it "raises a Timeout::Error" do + expect { klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) }.to raise_error(Timeout::Error) + end + + it "handles processes that do not shutdown correctly" do + expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) + end + end + + context 'timeout period' do + let(:time_taken) do + begin + start = Time.now + klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) + rescue + Time.now - start + end + end + + it { expect(time_taken).to be >= timeout } + end + + context 'clean up' do + let(:instance) { klass.new } + + it 'kills the child process' do + expect(instance).to receive(:kill_process_group_for_pid).and_wrap_original do |m, *args| + # is the PID, and it should not be running at this point + m.call(*args) + + pid = args.first + begin + Process.getpgid(pid) + raise "The child process should have been killed" + rescue Errno::ESRCH + end + end + + expect { instance.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) + end + end + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index b11fa38856b..b2d2f770e3d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1525,6 +1525,45 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#merge' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } + let(:user) { build(:user) } + let(:target_branch) { 'test-merge-target-branch' } + + before do + repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f') + end + + after do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + + shared_examples '#merge' do + it 'can perform a merge' do + merge_commit_id = nil + result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id| + merge_commit_id = commit_id + end + + expect(result.newrev).to eq(merge_commit_id) + expect(result.repo_created).to eq(false) + expect(result.branch_created).to eq(false) + end + end + + context 'with gitaly' do + it_behaves_like '#merge' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#merge' + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 80d92b2e6a3..121c0ed04ed 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -496,6 +496,7 @@ Timelog: - merge_request_id - issue_id - user_id +- spent_at - created_at - updated_at ProjectAutoDevops: diff --git a/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb new file mode 100644 index 00000000000..8b58f0b3725 --- /dev/null +++ b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe Gitlab::QuickActions::SpendTimeAndDateSeparator do + subject { described_class } + + shared_examples 'arg line with invalid parameters' do + it 'return nil' do + expect(subject.new(invalid_arg).execute).to eq(nil) + end + end + + shared_examples 'arg line with valid parameters' do + it 'return time and date array' do + expect(subject.new(valid_arg).execute).to eq(expected_response) + end + end + + describe '#execute' do + context 'invalid paramenter in arg line' do + context 'empty arg line' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '' } + end + end + + context 'future date in arg line' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '10m 6023-02-02' } + end + end + + context 'unparseable date(invalid mixes of delimiters)' do + it_behaves_like 'arg line with invalid parameters' do + let(:invalid_arg) { '10m 2017.02-02' } + end + end + + context 'trash in arg line' do + let(:invalid_arg) { 'dfjkghdskjfghdjskfgdfg' } + + it 'return nil as time value' do + time_date_response = subject.new(invalid_arg).execute + + expect(time_date_response).to be_an_instance_of(Array) + expect(time_date_response.first).to eq(nil) + end + end + end + + context 'only time present in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:valid_arg) { '2m 3m 5m 1h' } + let(:time) { Gitlab::TimeTrackingFormatter.parse(valid_arg) } + let(:date) { DateTime.now.to_date } + let(:expected_response) { [time, date] } + end + end + + context 'simple time with date in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:raw_time) { '10m' } + let(:raw_date) { '2016-02-02' } + let(:valid_arg) { "#{raw_time} #{raw_date}" } + let(:date) { Date.parse(raw_date) } + let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } + let(:expected_response) { [time, date] } + end + end + + context 'composite time with date in arg line' do + it_behaves_like 'arg line with valid parameters' do + let(:raw_time) { '2m 10m 1h 3d' } + let(:raw_date) { '2016/02/02' } + let(:valid_arg) { "#{raw_time} #{raw_date}" } + let(:date) { Date.parse(raw_date) } + let(:time) { Gitlab::TimeTrackingFormatter.parse(raw_time) } + let(:expected_response) { [time, date] } + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index c2e77ef6b6c..884f27b212c 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -39,6 +39,18 @@ describe Gitlab::SidekiqStatus do end end + describe '.running?', :clean_gitlab_redis_shared_state do + it 'returns true if job is running' do + described_class.set('123') + + expect(described_class.running?('123')).to be(true) + end + + it 'returns false if job is not found' do + expect(described_class.running?('123')).to be(false) + end + end + describe '.num_running', :clean_gitlab_redis_shared_state do it 'returns 0 if all jobs have been completed' do expect(described_class.num_running(%w(123))).to eq(0) diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 777e9c8e21d..a7b65e94706 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -61,6 +61,8 @@ describe Gitlab::UsageData do deployments environments gcp_clusters + gcp_clusters_enabled + gcp_clusters_disabled in_review_folder groups issues diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb index 926df21ffda..b9946c0315a 100644 --- a/spec/models/blob_viewer/readme_spec.rb +++ b/spec/models/blob_viewer/readme_spec.rb @@ -37,7 +37,7 @@ describe BlobViewer::Readme do context 'when the wiki is not empty' do before do - WikiPages::CreateService.new(project, project.owner, title: 'home', content: 'Home page').execute + create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) end it 'returns nil' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 06f76b5501e..41ecdb604f1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1743,19 +1743,34 @@ describe Ci::Build do end describe 'state transition when build fails' do + let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user) } + + before do + allow(MergeRequests::AddTodoWhenBuildFailsService).to receive(:new).and_return(service) + allow(service).to receive(:close) + end + context 'when build is configured to be retried' do - subject { create(:ci_build, :running, options: { retry: 3 }) } + subject { create(:ci_build, :running, options: { retry: 3 }, project: project, user: user) } - it 'retries builds and assigns a same user to it' do + it 'retries build and assigns the same user to it' do expect(described_class).to receive(:retry) - .with(subject, subject.user) + .with(subject, user) + + subject.drop! + end + + it 'does not try to create a todo' do + project.add_developer(user) + + expect(service).not_to receive(:commit_status_merge_requests) subject.drop! end end context 'when build is not configured to be retried' do - subject { create(:ci_build, :running) } + subject { create(:ci_build, :running, project: project, user: user) } it 'does not retry build' do expect(described_class).not_to receive(:retry) @@ -1770,6 +1785,14 @@ describe Ci::Build do subject.drop! end + + it 'creates a todo' do + project.add_developer(user) + + expect(service).to receive(:commit_status_merge_requests) + + subject.drop! + end end end end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index eb0a3e9e0d3..da972d2d86a 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -105,7 +105,7 @@ describe DiffNote do describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.formatter.new_line, 15) + line_code = Gitlab::Git.diff_line_code(position.file_path, position.formatter.new_line, 15) expect(subject.line_code).to eq(line_code) end diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb index 350fbc257d9..8f39fff6394 100644 --- a/spec/models/gcp/cluster_spec.rb +++ b/spec/models/gcp/cluster_spec.rb @@ -7,6 +7,30 @@ describe Gcp::Cluster do it { is_expected.to validate_presence_of(:gcp_cluster_zone) } + describe '.enabled' do + subject { described_class.enabled } + + let!(:cluster) { create(:gcp_cluster, enabled: true) } + + before do + create(:gcp_cluster, enabled: false) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe '.disabled' do + subject { described_class.disabled } + + let!(:cluster) { create(:gcp_cluster, enabled: false) } + + before do + create(:gcp_cluster, enabled: true) + end + + it { is_expected.to contain_exactly(cluster) } + end + describe '#default_value_for' do let(:cluster) { described_class.new } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 17c9f15b021..73e038b61ed 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1460,11 +1460,31 @@ describe MergeRequest do end describe '#merge_ongoing?' do - it 'returns true when merge_id is present and MR is not merged' do + it 'returns true when merge_id, MR is not merged and it has no running job' do merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo') + allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true } expect(merge_request.merge_ongoing?).to be(true) end + + it 'returns false when merge_jid is nil' do + merge_request = build_stubbed(:merge_request, state: :open, merge_jid: nil) + + expect(merge_request.merge_ongoing?).to be(false) + end + + it 'returns false if MR is merged' do + merge_request = build_stubbed(:merge_request, state: :merged, merge_jid: 'foo') + + expect(merge_request.merge_ongoing?).to be(false) + end + + it 'returns false if there is no merge job running' do + merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo') + allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { false } + + expect(merge_request.merge_ongoing?).to be(false) + end end describe "#closed_without_fork?" do diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index f89be20ad78..6a5d0decfec 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -108,12 +108,8 @@ describe MicrosoftTeamsService do message: "user created page: Awesome wiki_page" } end - - let(:wiki_page_sample_data) do - service = WikiPages::CreateService.new(project, user, opts) - wiki_page = service.execute - Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') - end + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) } + let(:wiki_page_sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') } it "calls Microsoft Teams API" do chat_service.execute(wiki_page_sample_data) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f44693a71bb..39d188f18af 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1286,21 +1286,31 @@ describe Repository do let(:message) { 'Test \r\n\r\n message' } - it 'merges the code and returns the commit id' do - expect(merge_commit).to be_present - expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present - end + shared_examples '#merge' do + it 'merges the code and returns the commit id' do + expect(merge_commit).to be_present + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end - it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do - merge_commit_id = merge(repository, user, merge_request, message) + it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do + merge_commit_id = merge(repository, user, merge_request, message) - expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) + expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) + end + + it 'removes carriage returns from commit message' do + merge_commit_id = merge(repository, user, merge_request, message) + + expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r")) + end end - it 'removes carriage returns from commit message' do - merge_commit_id = merge(repository, user, merge_request, message) + context 'with gitaly' do + it_behaves_like '#merge' + end - expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r")) + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '#merge' end def merge(repository, user, merge_request, message) diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb index 1a55e2a71cd..67624a0bbea 100644 --- a/spec/requests/api/v3/repositories_spec.rb +++ b/spec/requests/api/v3/repositories_spec.rb @@ -97,10 +97,11 @@ describe API::V3::Repositories do end end - { - 'blobs/:sha' => 'blobs/master', - 'commits/:sha/blob' => 'commits/master/blob' - }.each do |desc_path, example_path| + [ + ['blobs/:sha', 'blobs/master'], + ['blobs/:sha', 'blobs/v1.1.0'], + ['commits/:sha/blob', 'commits/master/blob'] + ].each do |desc_path, example_path| describe "GET /projects/:id/repository/#{desc_path}" do let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" } shared_examples_for 'repository blob' do @@ -110,7 +111,7 @@ describe API::V3::Repositories do end context 'when sha does not exist' do it_behaves_like '404 response' do - let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) } + let(:request) { get v3_api("/projects/#{project.id}/repository/#{desc_path.sub(':sha', 'invalid_branch_name')}?filepath=README.md", current_user) } let(:message) { '404 Commit Not Found' } end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 1c2d0b3e0dc..9128280eb5a 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -43,6 +43,21 @@ describe Auth::ContainerRegistryAuthenticationService do end end + shared_examples 'a browsable' do + let(:access) do + [{ 'type' => 'registry', + 'name' => 'catalog', + 'actions' => ['*'] }] + end + + it_behaves_like 'a valid token' + it_behaves_like 'not a container repository factory' + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end + end + shared_examples 'an accessible' do let(:access) do [{ 'type' => 'repository', @@ -51,7 +66,10 @@ describe Auth::ContainerRegistryAuthenticationService do end it_behaves_like 'a valid token' - it { expect(payload).to include('access' => access) } + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end end shared_examples 'an inaccessible' do @@ -117,6 +135,17 @@ describe Auth::ContainerRegistryAuthenticationService do context 'user authorization' do let(:current_user) { create(:user) } + context 'for registry catalog' do + let(:current_params) do + { scope: "registry:catalog:*" } + end + + context 'disallow browsing for users without Gitlab admin rights' do + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + context 'for private project' do let(:project) { create(:project) } @@ -490,6 +519,16 @@ describe Auth::ContainerRegistryAuthenticationService do end end + context 'registry catalog browsing authorized as admin' do + let(:current_user) { create(:user, :admin) } + + let(:current_params) do + { scope: "registry:catalog:*" } + end + + it_behaves_like 'a browsable' + end + context 'unauthorized' do context 'disallow to use scope-less authentication' do it_behaves_like 'a forbidden' @@ -536,5 +575,14 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end end + + context 'for registry catalog' do + let(:current_params) do + { scope: "registry:catalog:*" } + end + + it_behaves_like 'a forbidden' + it_behaves_like 'not a container repository factory' + end end end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 9db3568abee..b61d1cb765e 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -160,8 +160,9 @@ describe Ci::RetryBuildService do expect(new_build).to be_created end - it 'does mark old build as retried' do + it 'does mark old build as retried in the database and on the instance' do expect(new_build).to be_latest + expect(build).to be_retried expect(build.reload).to be_retried end end diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb index 23982b9e6e1..0b32c51a16f 100644 --- a/spec/services/merge_requests/conflicts/list_service_spec.rb +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -35,7 +35,7 @@ describe MergeRequests::Conflicts::ListService do it 'returns a falsey value when the MR has a missing ref after a force push' do merge_request = create_merge_request('conflict-resolvable') service = conflicts_service(merge_request) - allow(service.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError) + allow_any_instance_of(Rugged::Repository).to receive(:merge_commits).and_raise(Rugged::OdbError) expect(service.can_be_resolved_in_ui?).to be_falsey end diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index a1f7dc44d31..5376083e7f5 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -107,25 +107,27 @@ describe MergeRequests::Conflicts::ResolveService do branch_name: 'conflict-start') end - def resolve_conflicts + subject do described_class.new(merge_request_from_fork).execute(user, params) end it 'gets conflicts from the source project' do + # REFACTOR NOTE: We used to test that `project.repository.rugged` wasn't + # used in this case, but since the refactor, for simplification, + # we always use that repository for read only operations. expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original - expect(project.repository.rugged).not_to receive(:merge_commits) - resolve_conflicts + subject end it 'creates a commit with the message' do - resolve_conflicts + subject expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message]) end it 'creates a commit with the correct parents' do - resolve_conflicts + subject expect(merge_request_from_fork.source_branch_head.parents.map(&:id)) .to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head]) @@ -200,14 +202,19 @@ describe MergeRequests::Conflicts::ResolveService do } end - it 'raises a MissingResolution error' do + it 'raises a ResolutionError error' do expect { service.execute(user, invalid_params) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) end end context 'when the content of a file is unchanged' do - let(:list_service) { MergeRequests::Conflicts::ListService.new(merge_request) } + let(:resolver) do + MergeRequests::Conflicts::ListService.new(merge_request).conflicts.resolver + end + let(:regex_conflict) do + resolver.conflict_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb') + end let(:invalid_params) do { @@ -219,16 +226,16 @@ describe MergeRequests::Conflicts::ResolveService do }, { old_path: 'files/ruby/regex.rb', new_path: 'files/ruby/regex.rb', - content: list_service.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content + content: regex_conflict.content } ], commit_message: 'This is a commit message!' } end - it 'raises a MissingResolution error' do + it 'raises a ResolutionError error' do expect { service.execute(user, invalid_params) } - .to raise_error(Gitlab::Conflict::File::MissingResolution) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) end end @@ -246,9 +253,9 @@ describe MergeRequests::Conflicts::ResolveService do } end - it 'raises a MissingFiles error' do + it 'raises a ResolutionError error' do expect { service.execute(user, invalid_params) } - .to raise_error(described_class::MissingFiles) + .to raise_error(Gitlab::Git::Conflict::Resolver::ResolutionError) end end end diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 031366d1825..d4ac1f6ad81 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -52,6 +52,11 @@ describe Projects::UpdatePagesService do expect(project.pages_deployed?).to be_falsey expect(execute).to eq(:success) expect(project.pages_deployed?).to be_truthy + + # Check that all expected files are extracted + %w[index.html zero .hidden/file].each do |filename| + expect(File.exist?(File.join(project.public_pages_path, filename))).to be_truthy + end end it 'limits pages size' do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 6926ac85de3..c35177f6ebc 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -207,7 +207,11 @@ describe QuickActions::InterpretService do it 'populates spend_time: 3600 if content contains /spend 1h' do _, updates = service.execute(content, issuable) - expect(updates).to eq(spend_time: { duration: 3600, user: developer }) + expect(updates).to eq(spend_time: { + duration: 3600, + user: developer, + spent_at: DateTime.now.to_date + }) end end @@ -215,7 +219,39 @@ describe QuickActions::InterpretService do it 'populates spend_time: -1800 if content contains /spend -30m' do _, updates = service.execute(content, issuable) - expect(updates).to eq(spend_time: { duration: -1800, user: developer }) + expect(updates).to eq(spend_time: { + duration: -1800, + user: developer, + spent_at: DateTime.now.to_date + }) + end + end + + shared_examples 'spend command with valid date' do + it 'populates spend time: 1800 with date in date type format' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { + duration: 1800, + user: developer, + spent_at: Date.parse(date) + }) + end + end + + shared_examples 'spend command with invalid date' do + it 'will not create any note and timelog' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq({}) + end + end + + shared_examples 'spend command with future date' do + it 'will not create any note and timelog' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq({}) end end @@ -669,6 +705,22 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + it_behaves_like 'spend command with valid date' do + let(:date) { '2016-02-02' } + let(:content) { "/spend 30m #{date}" } + let(:issuable) { issue } + end + + it_behaves_like 'spend command with invalid date' do + let(:content) { '/spend 30m 17-99-99' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command with future date' do + let(:content) { '/spend 30m 6017-10-10' } + let(:issuable) { issue } + end + it_behaves_like 'empty command' do let(:content) { '/spend' } let(:issuable) { issue } diff --git a/spec/support/board_helpers.rb b/spec/support/board_helpers.rb new file mode 100644 index 00000000000..507d0432d7f --- /dev/null +++ b/spec/support/board_helpers.rb @@ -0,0 +1,16 @@ +module BoardHelpers + def click_card(card) + within card do + first('.card-number').click + end + + wait_for_sidebar + end + + def wait_for_sidebar + # loop until the CSS transition is complete + Timeout.timeout(0.5) do + loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290 + end + end +end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 6accf16bea4..17f3a861ba8 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -76,8 +76,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do message: "user created page: Awesome wiki_page" } - wiki_page_service = WikiPages::CreateService.new(project, user, opts) - @wiki_page = wiki_page_service.execute + @wiki_page = create(:wiki_page, wiki: project.wiki, attrs: opts) @wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create') end diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 24623ff4c1f..9f78059986d 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -13,9 +13,9 @@ activemodel,4.2.8,MIT activerecord,4.2.8,MIT activesupport,4.2.8,MIT acts-as-taggable-on,4.0.0,MIT -addressable,2.3.8,Apache 2.0 +addressable,2.5.2,Apache 2.0 after,0.8.2,MIT -ajv,5.2.0,MIT +ajv,5.2.2,MIT ajv-keywords,2.1.0,MIT akismet,2.0.0,MIT align-text,0.1.4,MIT @@ -28,8 +28,6 @@ ansi-regex,2.1.1,MIT ansi-styles,2.2.1,MIT anymatch,1.3.2,ISC append-transform,0.4.0,MIT -aproba,1.1.1,ISC -are-we-there-yet,1.1.4,ISC arel,6.0.4,MIT argparse,1.0.9,MIT arr-diff,2.0.0,MIT @@ -46,21 +44,15 @@ arrify,1.0.1,MIT asana,0.6.0,MIT asciidoctor,1.5.3,MIT asciidoctor-plantuml,0.0.7,MIT -asn1,0.2.3,MIT asn1.js,4.9.1,MIT assert,1.4.1,MIT -assert-plus,0.2.0,MIT async,2.4.1,MIT async-each,1.0.1,MIT -asynckit,0.4.0,MIT atomic,1.1.99,Apache 2.0 attr_encrypted,3.0.3,MIT attr_required,1.0.0,MIT -autoparse,0.3.3,Apache 2.0 autoprefixer,6.7.7,MIT autoprefixer-rails,6.2.3,MIT -aws-sign2,0.6.0,Apache 2.0 -aws4,1.6.0,MIT axiom-types,0.1.1,MIT axios,0.16.2,MIT babel-code-frame,6.22.0,MIT @@ -145,19 +137,16 @@ base64-js,1.2.0,MIT base64id,1.0.0,MIT batch,0.6.1,MIT bcrypt,3.1.11,MIT -bcrypt-pbkdf,1.0.1,New BSD bcrypt_pbkdf,1.0.0,MIT better-assert,1.0.2,MIT big.js,3.1.3,MIT binary-extensions,1.10.0,MIT -bindata,2.3.5,ruby +bindata,2.4.1,ruby blob,0.0.4,unknown -block-stream,0.0.9,ISC bluebird,2.11.0,MIT bn.js,4.11.6,MIT body-parser,1.17.2,MIT bonjour,3.5.0,MIT -boom,2.10.1,New BSD bootstrap-sass,3.3.6,MIT bootstrap_form,2.7.0,MIT brace-expansion,1.1.8,MIT @@ -187,7 +176,6 @@ camelcase-keys,2.1.0,MIT caniuse-api,1.6.1,MIT caniuse-db,1.0.30000649,CC-BY-4.0 carrierwave,1.1.0,MIT -caseless,0.12.0,Apache 2.0 cause,0.1,MIT center-align,0.1.3,MIT chalk,1.1.3,MIT @@ -216,7 +204,6 @@ color-string,0.3.0,MIT colormin,1.1.2,MIT colors,1.1.2,MIT combine-lists,1.0.1,MIT -combined-stream,1.0.5,MIT commander,2.9.0,MIT commondir,1.0.1,MIT component-bind,1.0.0,unknown @@ -233,8 +220,7 @@ configstore,1.4.0,Simplified BSD connect,3.6.3,MIT connect-history-api-fallback,1.3.0,MIT connection_pool,2.2.1,MIT -console-browserify,1.1.0,MIT -console-control-strings,1.1.0,ISC +console-browserify,1.1.0,[Circular] consolidate,0.14.5,MIT constants-browserify,1.0.0,MIT contains-path,0.1.0,MIT @@ -254,7 +240,6 @@ create-hmac,1.1.4,MIT creole,0.5.0,ruby cropper,2.3.0,MIT cross-spawn,5.1.0,MIT -cryptiles,2.0.5,New BSD crypto-browserify,3.11.0,MIT css-color-names,0.0.4,MIT css-loader,0.28.0,MIT @@ -268,13 +253,14 @@ custom-event,1.0.1,MIT d,1.0.0,MIT d3,3.5.11,New BSD d3_rails,3.5.11,MIT -dashdash,1.14.1,MIT date-now,0.1.4,MIT de-indent,1.0.2,MIT debug,2.6.8,MIT debugger-ruby_core_source,1.3.8,MIT decamelize,1.2.0,MIT deckar01-task_list,2.0.0,MIT +declarative,0.0.10,MIT +declarative-option,0.1.0,MIT decompress-response,3.3.0,MIT deep-equal,1.0.1,MIT deep-extend,0.4.2,MIT @@ -283,9 +269,7 @@ default-require-extensions,1.0.0,MIT default_value_for,3.0.2,MIT defined,1.0.0,MIT del,2.2.2,MIT -delayed-stream,1.0.0,MIT delegate,3.1.2,MIT -delegates,1.0.0,MIT depd,1.1.1,MIT des.js,1.0.0,MIT descendants_tracker,0.0.4,MIT @@ -310,14 +294,13 @@ domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" domelementtype,1.3.0,unknown domhandler,2.3.0,unknown domutils,1.5.1,unknown -doorkeeper,4.2.0,MIT -doorkeeper-openid_connect,1.1.2,MIT +doorkeeper,4.2.6,MIT +doorkeeper-openid_connect,1.2.0,MIT dropzone,4.2.0,MIT dropzonejs-rails,0.7.2,MIT -duplexer,0.1.1,MIT +duplexer,0.1.1,[Circular] duplexer3,0.1.4,New BSD duplexify,3.5.1,MIT -ecc-jsbn,0.1.1,MIT editorconfig,0.13.2,MIT ee-first,1.1.1,MIT ejs,2.5.6,Apache 2.0 @@ -362,7 +345,7 @@ eslint-plugin-import,2.2.0,MIT eslint-plugin-jasmine,2.2.0,MIT eslint-plugin-promise,3.5.0,ISC espree,3.5.0,Simplified BSD -esprima,4.0.0,Simplified BSD +esprima,2.7.3,Simplified BSD esquery,1.0.0,BSD esrecurse,4.1.0,Simplified BSD estraverse,4.1.1,Simplified BSD @@ -388,12 +371,10 @@ express,4.15.4,MIT expression_parser,0.9.0,MIT extend,3.0.1,MIT extglob,0.3.2,MIT -extlib,0.9.16,MIT -extsprintf,1.0.2,MIT -faraday,0.12.1,MIT +faraday,0.12.2,MIT faraday_middleware,0.11.0.1,MIT faraday_middleware-multi_json,0.0.6,MIT -fast-deep-equal,0.1.0,MIT +fast-deep-equal,1.0.0,MIT fast-levenshtein,2.0.6,MIT fast_gettext,1.4.0,"MIT,ruby" fastparse,1.1.1,MIT @@ -428,8 +409,6 @@ follow-redirects,1.2.3,MIT font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License" for-in,0.1.6,MIT for-own,0.1.4,MIT -forever-agent,0.6.1,Apache 2.0 -form-data,2.1.4,MIT formatador,0.2.5,MIT forwarded,0.1.0,MIT fresh,0.5.0,MIT @@ -437,11 +416,8 @@ from,0.1.7,MIT fs-access,1.0.1,MIT fs-extra,0.26.7,MIT fs.realpath,1.0.0,ISC -fsevents,1.1.2,MIT -fstream,1.0.11,ISC -fstream-ignore,1.0.5,ISC +fsevents,,unknown function-bind,1.1.0,MIT -gauge,2.7.4,ISC gemnasium-gitlab-service,0.2.6,MIT gemojione,3.3.0,MIT generate-function,2.0.0,MIT @@ -450,15 +426,15 @@ get-caller-file,1.0.2,ISC get-stdin,4.0.1,MIT get-stream,3.0.0,MIT get_process_mem,0.2.0,MIT -getpass,0.1.7,MIT gettext_i18n_rails,1.8.0,MIT gettext_i18n_rails_js,1.2.0,MIT -gitaly-proto,0.33.0,MIT +gitaly-proto,0.41.0,MIT github-linguist,4.7.6,MIT github-markup,1.6.1,MIT gitlab-flowdock-git-hook,1.0.1,MIT -gitlab-grit,2.8.1,MIT -gitlab-markup,1.5.1,MIT +gitlab-grit,2.8.2,MIT +gitlab-markup,1.6.2,MIT +gitlab-svgs,1.0.4,unknown gitlab_omniauth-ldap,2.0.4,MIT glob,6.0.4,ISC glob-base,0.3.0,MIT @@ -471,9 +447,9 @@ gollum-lib,4.2.7,MIT gollum-rugged_adapter,0.4.4,MIT gon,6.1.0,MIT good-listener,1.2.2,MIT -google-api-client,0.8.7,Apache 2.0 +google-api-client,0.13.6,Apache 2.0 google-protobuf,3.4.0.2,New BSD -googleauth,0.5.1,Apache 2.0 +googleauth,0.5.3,Apache 2.0 got,7.1.0,MIT gpgme,2.0.13,LGPL-2.1+ graceful-fs,4.1.11,ISC @@ -481,14 +457,12 @@ graceful-readlink,1.0.1,MIT grape,1.0.0,MIT grape-entity,0.6.0,MIT grape-route-helpers,2.1.0,MIT -grape_logging,1.6.0,MIT -grpc,1.4.5,New BSD +grape_logging,1.7.0,MIT +grpc,1.6.0,Apache 2.0 gzip-size,3.0.0,MIT hamlit,2.6.1,MIT handle-thing,1.2.5,MIT handlebars,4.0.6,MIT -har-schema,1.0.5,ISC -har-validator,4.2.1,ISC has,1.0.1,MIT has-ansi,2.0.0,MIT has-binary,0.1.7,MIT @@ -496,16 +470,13 @@ has-cors,1.1.0,MIT has-flag,2.0.0,MIT has-symbol-support-x,1.3.0,MIT has-to-string-tag-x,1.3.0,MIT -has-unicode,2.0.1,ISC hash-sum,1.0.2,MIT hash.js,1.0.3,MIT hashie,3.5.6,MIT hashie-forbidden_attributes,0.1.1,MIT -hawk,3.1.3,New BSD he,1.1.1,MIT health_check,2.6.0,MIT hipchat,1.5.2,MIT -hoek,2.16.3,New BSD home-or-tmp,2.0.0,MIT hosted-git-info,2.2.0,ISC hpack.js,2.1.6,MIT @@ -522,7 +493,6 @@ http-errors,1.6.2,MIT http-form_data,1.0.1,MIT http-proxy,1.16.2,MIT http-proxy-middleware,0.17.4,MIT -http-signature,1.1.1,MIT http_parser.rb,0.6.0,MIT httparty,0.13.7,MIT httpclient,2.8.2,ruby @@ -583,15 +553,13 @@ is-resolvable,1.0.0,MIT is-retry-allowed,1.1.0,MIT is-stream,1.1.0,MIT is-svg,2.1.0,MIT -is-typedarray,1.0.0,MIT is-unc-path,0.1.2,MIT is-utf8,0.2.1,MIT is-windows,0.2.0,MIT isarray,1.0.0,MIT isbinaryfile,3.0.2,MIT -isexe,1.1.2,ISC +isexe,2.0.0,ISC isobject,2.1.0,MIT -isstream,0.1.2,MIT istanbul,0.4.5,New BSD istanbul-api,1.1.1,New BSD istanbul-lib-coverage,1.0.1,New BSD @@ -605,7 +573,6 @@ jasmine-core,2.6.3,MIT jasmine-jquery,2.1.1,MIT jed,1.1.1,MIT jira-ruby,1.4.1,MIT -jodid25519,1.0.2,MIT jquery,2.2.1,MIT jquery-atwho-rails,1.3.2,MIT jquery-rails,4.1.1,MIT @@ -615,21 +582,18 @@ js-beautify,1.6.12,MIT js-cookie,2.1.3,MIT js-tokens,3.0.1,MIT js-yaml,3.7.0,MIT -jsbn,0.1.1,MIT jsesc,1.3.0,MIT json,1.8.6,ruby -json-jwt,1.7.1,MIT +json-jwt,1.7.2,MIT json-loader,0.5.7,MIT -json-schema,0.2.3,"AFLv2.1,BSD" json-schema-traverse,0.3.1,MIT json-stable-stringify,1.0.1,MIT json-stringify-safe,5.0.1,ISC -json3,3.3.2,MIT +json3,3.3.2,[Circular] json5,0.5.1,MIT jsonfile,2.4.0,MIT jsonify,0.0.0,Public Domain jsonpointer,4.0.1,MIT -jsprim,1.4.0,MIT jszip,3.1.3,(MIT OR GPL-3.0) jszip-utils,0.0.2,MIT or GPLv3 jwt,1.5.6,MIT @@ -649,7 +613,6 @@ kind-of,3.1.0,MIT klaw,1.3.1,MIT kubeclient,2.2.0,MIT latest-version,1.0.1,MIT -launchy,2.4.3,ISC lazy-cache,1.0.4,MIT lcid,1.0.0,MIT levn,0.3.0,MIT @@ -706,7 +669,7 @@ marked,0.3.6,MIT math-expression-evaluator,1.2.16,MIT media-typer,0.3.0,MIT mem,1.1.0,MIT -memoist,0.15.0,MIT +memoist,0.16.0,MIT memory-fs,0.4.1,MIT meow,3.7.0,MIT merge-descriptors,1.0.1,MIT @@ -714,13 +677,14 @@ method_source,0.8.2,MIT methods,1.1.2,MIT micromatch,2.3.11,MIT miller-rabin,4.0.0,MIT -mime,1.3.4,MIT -mime-db,1.27.0,MIT -mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0" +mime,1.3.4,[Circular] +mime-db,1.29.0,MIT +mime-types,3.1,MIT +mime-types-data,3.2016.0521,MIT mimemagic,0.3.0,MIT mimic-fn,1.1.0,MIT mimic-response,1.0.0,MIT -mini_portile2,2.2.0,MIT +mini_portile2,2.3.0,MIT minimalistic-assert,1.0.0,ISC minimatch,3.0.3,ISC minimist,0.0.8,MIT @@ -731,7 +695,7 @@ monaco-editor,0.8.3,MIT mousetrap,1.4.6,Apache 2.0 mousetrap-rails,1.4.6,"MIT,Apache" ms,2.0.0,MIT -multi_json,1.12.1,MIT +multi_json,1.12.2,MIT multi_xml,0.6.0,MIT multicast-dns,6.1.1,MIT multicast-dns-service-types,1.1.0,MIT @@ -739,9 +703,7 @@ multipart-post,2.0.0,MIT mustermann,1.0.0,MIT mustermann-grape,1.0.0,MIT mute-stream,0.0.5,ISC -mysql2,0.4.5,MIT name-all-modules-plugin,1.0.1,MIT -nan,2.6.2,MIT natural-compare,1.4.0,MIT negotiator,0.6.1,MIT nested-error-stacks,1.0.2,MIT @@ -751,22 +713,19 @@ netrc,0.11.0,MIT node-dir,0.1.17,MIT node-forge,0.6.33,BSD node-libs-browser,2.0.0,MIT -node-pre-gyp,0.6.36,New BSD nodemon,1.11.0,MIT -nokogiri,1.8.0,MIT +nokogiri,1.8.1,MIT nopt,3.0.6,ISC -normalize-package-data,2.4.0,Simplified BSD +normalize-package-data,2.3.5,Simplified BSD normalize-path,2.1.1,MIT normalize-range,0.1.2,MIT normalize-url,1.9.1,MIT npm-run-path,2.0.2,MIT -npmlog,4.1.0,ISC null-check,1.0.0,MIT num2fraction,1.2.2,MIT number-is-nan,1.0.1,MIT numerizer,0.1.1,MIT oauth,0.5.1,MIT -oauth-sign,0.8.2,Apache 2.0 oauth2,1.4.0,MIT object-assign,4.1.1,MIT object-component,0.0.3,unknown @@ -777,7 +736,7 @@ oj,2.17.5,MIT omniauth,1.4.2,MIT omniauth-auth0,1.4.1,MIT omniauth-authentiq,0.3.1,MIT -omniauth-azure-oauth2,0.0.6,MIT +omniauth-azure-oauth2,0.0.9,MIT omniauth-cas3,1.1.4,MIT omniauth-facebook,4.0.0,MIT omniauth-github,1.1.2,MIT @@ -786,7 +745,7 @@ omniauth-google-oauth2,0.5.2,MIT omniauth-kerberos,0.3.0,MIT omniauth-multipassword,0.4.2,MIT omniauth-oauth,1.1.0,MIT -omniauth-oauth2,1.3.1,MIT +omniauth-oauth2,1.4.0,MIT omniauth-oauth2-generic,0.2.2,MIT omniauth-saml,1.7.0,MIT omniauth-shibboleth,1.2.1,MIT @@ -839,13 +798,11 @@ pbkdf2,3.0.9,MIT peek,1.0.1,MIT peek-gc,0.0.2,MIT peek-host,1.0.0,MIT -peek-mysql2,1.1.0,MIT peek-performance_bar,1.3.0,MIT peek-pg,1.3.0,MIT peek-rblineprof,0.2.0,MIT peek-redis,1.2.0,MIT peek-sidekiq,1.0.3,MIT -performance-now,0.2.0,MIT pg,0.18.4,"BSD,ruby,GPL" pify,2.3.0,MIT pikaday,1.5.1,"BSD,MIT" @@ -910,6 +867,7 @@ prr,0.0.0,MIT ps-tree,1.1.0,MIT pseudomap,1.0.2,ISC public-encrypt,4.0.0,MIT +public_suffix,3.0.0,MIT punycode,1.4.1,MIT pyu-ruby-sasl,0.0.3.3,MIT q,1.5.0,MIT @@ -917,7 +875,7 @@ qjobs,1.1.5,MIT qs,6.5.0,New BSD query-string,4.3.2,MIT querystring,0.2.0,MIT -querystring-es3,0.2.1,MIT +querystring-es3,0.2.1,[Circular] querystringify,0.0.4,MIT rack,1.6.8,MIT rack-accept,0.4.5,MIT @@ -935,7 +893,7 @@ rails-i18n,4.0.9,MIT railties,4.2.8,MIT rainbow,2.2.2,MIT raindrops,0.18.0,LGPL-2.1+ -rake,12.0.0,MIT +rake,12.1.0,MIT randomatic,1.1.6,MIT randombytes,2.0.3,MIT range-parser,1.2.0,MIT @@ -982,7 +940,7 @@ remove-trailing-separator,1.1.0,ISC repeat-element,1.1.2,MIT repeat-string,1.6.1,MIT repeating,2.0.1,MIT -request,2.81.0,Apache 2.0 +representable,3.0.4,MIT request_store,1.3.1,MIT require-directory,2.1.1,MIT require-from-string,1.2.1,MIT @@ -994,7 +952,7 @@ resolve-from,1.0.1,MIT responders,2.3.0,MIT rest-client,2.0.0,MIT restore-cursor,1.0.1,MIT -retriable,1.4.1,MIT +retriable,3.1.1,MIT right-align,0.1.3,MIT rimraf,2.6.1,ISC rinku,2.0.0,ISC @@ -1013,7 +971,7 @@ rufus-scheduler,3.4.0,MIT rugged,0.26.0,MIT run-async,0.1.0,MIT rx-lite,3.1.2,Apache 2.0 -safe-buffer,5.1.1,MIT +safe-buffer,5.0.1,MIT safe_yaml,1.0.4,MIT sanitize,2.1.0,MIT sass,3.4.22,MIT @@ -1053,7 +1011,6 @@ slack-notifier,1.5.1,MIT slash,1.0.0,MIT slice-ansi,0.0.4,MIT slide,1.1.6,ISC -sntp,1.0.9,BSD socket.io,1.7.3,MIT socket.io-adapter,0.5.0,MIT socket.io-client,1.7.3,MIT @@ -1074,7 +1031,6 @@ sprintf-js,1.0.3,New BSD sprockets,3.7.1,MIT sprockets-rails,3.2.0,MIT sql.js,0.4.0,MIT -sshpk,1.13.0,MIT state_machines,0.4.0,MIT state_machines-activemodel,0.4.0,MIT state_machines-activerecord,0.4.0,MIT @@ -1085,22 +1041,20 @@ stream-http,2.6.3,MIT stream-shift,1.0.0,MIT strict-uri-encode,1.1.0,MIT string-length,1.0.1,MIT -string-width,2.0.0,MIT -string_decoder,1.0.3,MIT +string-width,1.0.2,MIT +string_decoder,0.10.31,MIT stringex,2.7.1,MIT -stringstream,0.0.5,MIT strip-ansi,3.0.1,MIT strip-bom,3.0.0,MIT strip-eof,1.0.0,MIT strip-indent,1.0.1,MIT strip-json-comments,2.0.1,MIT -supports-color,4.2.1,MIT +supports-color,3.2.3,MIT +svg4everybody,2.1.9,CC0-1.0 svgo,0.7.2,MIT sys-filesystem,1.1.6,Artistic 2.0 table,3.8.3,New BSD tapable,0.2.8,MIT -tar,2.2.1,ISC -tar-pack,3.4.0,Simplified BSD temple,0.7.7,MIT test-exclude,4.0.0,ISC text,1.3.1,MIT @@ -1113,6 +1067,7 @@ three-stl-loader,1.0.4,MIT through,2.3.8,MIT thunky,0.1.0,unknown tilt,2.0.6,MIT +time-stamp,2.0.0,MIT timeago.js,2.0.5,MIT timed-out,4.0.1,MIT timers-browserify,2.0.4,MIT @@ -1124,31 +1079,28 @@ to-arraybuffer,1.0.1,MIT to-fast-properties,1.0.2,MIT toml-rb,0.3.15,MIT touch,1.0.0,ISC -tough-cookie,2.3.2,New BSD traverse,0.6.6,MIT trim-newlines,1.0.0,MIT trim-right,1.0.1,MIT truncato,0.7.10,MIT tryit,1.0.3,MIT tty-browserify,0.0.0,MIT -tunnel-agent,0.6.0,Apache 2.0 -tweetnacl,0.14.5,Unlicense type-check,0.3.2,MIT type-is,1.6.15,MIT typedarray,0.0.6,MIT tzinfo,1.2.3,MIT u2f,0.2.1,MIT +uber,0.1.0,MIT uglifier,2.7.2,MIT uglify-js,2.8.29,Simplified BSD uglify-to-browserify,1.0.2,MIT uglifyjs-webpack-plugin,0.4.6,MIT -uid-number,0.0.6,ISC ultron,1.1.0,MIT unc-path-regex,0.1.2,MIT undefsafe,0.0.3,MIT / http://rem.mit-license.org underscore,1.8.3,MIT unf,0.1.4,BSD -unf_ext,0.0.7.2,MIT +unf_ext,0.0.7.4,MIT unicorn,5.1.0,ruby unicorn-worker-killer,0.4.4,ruby uniq,1.0.1,MIT @@ -1166,13 +1118,12 @@ user-home,2.0.0,MIT useragent,2.2.1,MIT util,0.10.3,MIT util-deprecate,1.0.2,MIT -utils-merge,1.0.0,MIT -uuid,3.0.1,MIT +utils-merge,1.0.0,[Circular] +uuid,2.0.3,MIT validate-npm-package-license,3.0.1,Apache 2.0 validates_hostname,1.0.6,MIT vary,1.1.1,MIT vendors,1.0.1,MIT -verror,1.3.6,MIT version_sorter,2.1.0,MIT virtus,1.0.5,MIT visibilityjs,1.2.4,MIT @@ -1200,12 +1151,11 @@ webpack-stats-plugin,0.1.5,MIT websocket-driver,0.6.5,MIT websocket-extensions,0.1.1,MIT whet.extend,0.9.9,MIT -which,1.2.12,ISC +which,1.3.0,ISC which-module,2.0.0,ISC -wide-align,1.1.2,ISC wikicloth,0.8.1,MIT window-size,0.1.0,MIT -wordwrap,0.0.2,MIT/X11 +wordwrap,1.0.0,MIT wrap-ansi,2.1.0,MIT wrappy,1.0.2,ISC write,0.2.1,MIT diff --git a/yarn.lock b/yarn.lock index 57644482b32..b830278eab0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4148,9 +4148,9 @@ moment@2.x: version "2.17.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" -monaco-editor@0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.8.3.tgz#523bdf2d1524db2c2dfc3cae0a7b6edc48d6dea6" +monaco-editor@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.10.0.tgz#6604932585fe9c1f993f000a503d0d20fbe5896a" mousetrap@^1.4.6: version "1.4.6" @@ -6405,9 +6405,9 @@ vue@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed" -vuex@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6" +vuex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.0.tgz#98b4b5c4954b1c1c1f5b29fa0476a23580315814" watchpack@^1.4.0: version "1.4.0" |