diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ace0de5cad3db69220c873bb934fdb9ef06febe5..76117a48730901331b3f87e324ce4dcff8e29224 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,7 +22,7 @@ before_script: - bundle --version - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - retry gem install knapsack - - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' + - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql' stages: - prepare diff --git a/CHANGELOG.md b/CHANGELOG.md index dd88b348734005ab185808bded1971f2d45e9c81..494089490587875943ee78ca594a78fe145dcd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Update runner version only when updating contacted_at - Add link from system note to compare with previous version - Use gitlab-shell v3.6.6 + - Ability to resolve merge request conflicts with editor !6374 - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup @@ -19,9 +20,9 @@ Please view this file on the master branch, on stable branches it's out of date. - Clarify documentation for Runners API (Gennady Trafimenkov) - The instrumentation for Banzai::Renderer has been restored - Change user & group landing page routing from /u/:username to /:username - - Prevent running GfmAutocomplete setup for each diff note !6569 - Added documentation for .gitattributes files - Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342 + - Move Pipeline Metrics to separate worker - AbstractReferenceFilter caches project_refs on RequestStore when active - Replaced the check sign to arrow in the show build view. !6501 - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) @@ -40,7 +41,6 @@ Please view this file on the master branch, on stable branches it's out of date. - Update Gitlab Shell to fix some problems with moving projects between storages - Cache rendered markdown in the database, rather than Redis - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - - Do not alter 'force_remove_source_branch' options on MergeRequest unless specified - Simplify Mentionable concern instance methods - API: Ability to retrieve version information (Robert Schilling) - Fix permission for setting an issue's due date @@ -57,6 +57,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Added soft wrap button to repository file/blob editor - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - Show the time ago a merge request was deployed to an environment + - Add RTL support to markdown renderer (Ebrahim Byagowi) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps) - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) @@ -76,14 +77,12 @@ Please view this file on the master branch, on stable branches it's out of date. - Only update issuable labels if they have been changed - Take filters in account in issuable counters. !6496 - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) - - Prevent flash alert text from being obscured when container is fluid - Append issue template to existing description !6149 (Joseph Frazier) - Trending projects now only show public projects and the list of projects is cached for a day - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) - Revoke button in Applications Settings underlines on hover. - Use higher size on Gitlab::Redis connection pool on Sidekiq servers - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska) - - Fix Long commit messages overflow viewport in file tree - Revert avoid touching file system on Build#artifacts? - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created - Add disabled delete button to protected branches (ClemMakesApps) @@ -94,7 +93,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Replace bootstrap caret with fontawesome caret (ClemMakesApps) - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533 - Add organization field to user profile - - Ignore deployment for statistics in Cycle Analytics, except in staging and production stages + - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being. - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts) - Fix deploy status responsiveness error !6633 - Make searching for commits case insensitive @@ -124,8 +123,15 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.12.7 - - Use gitlab-markup gem instead of github-markup to fix `.rst` file rendering. !6659 - - Fix GFM autocomplete setup being called several times + - Prevent running `GfmAutocomplete` setup for each diff note. !6569 + - Fix long commit messages overflow viewport in file tree. !6573 + - Use `gitlab-markup` gem instead of `github-markup` to fix `.rst` file rendering. !6659 + - Prevent flash alert text from being obscured when container is fluid. !6694 + - Fix due date being displayed as `NaN` in Safari. !6797 + - Fix JS bug with select2 because of missing `data-field` attribute in select box. !6812 + - Do not alter `force_remove_source_branch` options on MergeRequest unless specified. !6817 + - Fix GFM autocomplete setup being called several times. !6840 + - Handle case where deployment ref no longer exists. !6855 ## 8.12.6 diff --git a/Gemfile b/Gemfile index 5f754c1b66fe5d272781b42d27dfc3ce88f47ae5..05166b6a828b5ef2812512122ea141158bb6d993 100644 --- a/Gemfile +++ b/Gemfile @@ -262,8 +262,6 @@ group :development do # thin instead webrick gem 'thin', '~> 1.7.0' - - gem 'activerecord_sane_schema_dumper', '0.2' end group :development, :test do @@ -310,6 +308,8 @@ group :development, :test do gem 'license_finder', '~> 2.1.0', require: false gem 'knapsack', '~> 1.11.0' + + gem 'activerecord_sane_schema_dumper', '0.2' end group :test do diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index f3957ed374b54b020802180a55a558a9b0674898..73691f40c74e6d45f04ea39f125f1abed85c8593 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -50,7 +50,7 @@ case 'projects:milestones:new': case 'projects:milestones:edit': new ZenMode(); - new DueDateSelect(); + new gl.DueDateSelectors(); new GLForm($('.milestone-form')); break; case 'groups:milestones:new': @@ -101,9 +101,6 @@ new ZenMode(); new MergedButtons(); break; - case "projects:merge_requests:conflicts": - window.mcui = new MergeConflictResolver() - break; case 'projects:merge_requests:index': shortcut_handler = new ShortcutsNavigation(); Issuable.init(); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js deleted file mode 100644 index bf68b7e3a9b757a3d51293645b906c6d5960a1df..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/due_date_select.js +++ /dev/null @@ -1,107 +0,0 @@ -(function() { - this.DueDateSelect = (function() { - function DueDateSelect() { - var $datePicker, $dueDate, $loading; - // Milestone edit/new form - $datePicker = $('.datepicker'); - if ($datePicker.length) { - $dueDate = $('#milestone_due_date'); - $datePicker.datepicker({ - dateFormat: 'yy-mm-dd', - onSelect: function(dateText, inst) { - return $dueDate.val(dateText); - } - }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())); - } - $('.js-clear-due-date').on('click', function(e) { - e.preventDefault(); - return $.datepicker._clearDate($datePicker); - }); - // Issuable sidebar - $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); - $('.js-due-date-select').each(function(i, dropdown) { - var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL; - $dropdown = $(dropdown); - $dropdownParent = $dropdown.closest('.dropdown'); - $datePicker = $dropdownParent.find('.js-due-date-calendar'); - $block = $dropdown.closest('.block'); - $selectbox = $dropdown.closest('.selectbox'); - $value = $block.find('.value'); - $valueContent = $block.find('.value-content'); - $sidebarValue = $('.js-due-date-sidebar-value', $block); - fieldName = $dropdown.data('field-name'); - abilityName = $dropdown.data('ability-name'); - issueUpdateURL = $dropdown.data('issue-update'); - $dropdown.glDropdown({ - hidden: function() { - $selectbox.hide(); - return $value.css('display', ''); - } - }); - addDueDate = function(isDropdown) { - var data, date, mediumDate, value; - // Create the post date - value = $("input[name='" + fieldName + "']").val(); - if (value !== '') { - date = new Date(value.replace(new RegExp('-', 'g'), ',')); - mediumDate = $.datepicker.formatDate('M d, yy', date); - } else { - mediumDate = 'No due date'; - } - data = {}; - data[abilityName] = {}; - data[abilityName].due_date = value; - return $.ajax({ - type: 'PUT', - url: issueUpdateURL, - data: data, - dataType: 'json', - beforeSend: function() { - var cssClass; - $loading.fadeIn(); - if (isDropdown) { - $dropdown.trigger('loading.gl.dropdown'); - $selectbox.hide(); - } - $value.css('display', ''); - cssClass = Date.parse(mediumDate) ? 'bold' : 'no-value'; - $valueContent.html("<span class='" + cssClass + "'>" + mediumDate + "</span>"); - $sidebarValue.html(mediumDate); - if (value !== '') { - return $('.js-remove-due-date-holder').removeClass('hidden'); - } else { - return $('.js-remove-due-date-holder').addClass('hidden'); - } - } - }).done(function(data) { - if (isDropdown) { - $dropdown.trigger('loaded.gl.dropdown'); - $dropdown.dropdown('toggle'); - } - return $loading.fadeOut(); - }); - }; - $block.on('click', '.js-remove-due-date', function(e) { - e.preventDefault(); - $("input[name='" + fieldName + "']").val(''); - return addDueDate(false); - }); - return $datePicker.datepicker({ - dateFormat: 'yy-mm-dd', - defaultDate: $("input[name='" + fieldName + "']").val(), - altField: "input[name='" + fieldName + "']", - onSelect: function() { - return addDueDate(true); - } - }); - }); - $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', function(e) { - return e.stopImmediatePropagation(); - }); - } - - return DueDateSelect; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..41925fcc8e3baaa0daa3b8bc21adc23619f00d05 --- /dev/null +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -0,0 +1,161 @@ +(function(global) { + class DueDateSelect { + constructor({ $dropdown, $loading } = {}) { + const $dropdownParent = $dropdown.closest('.dropdown'); + const $block = $dropdown.closest('.block'); + this.$loading = $loading; + this.$dropdown = $dropdown; + this.$dropdownParent = $dropdownParent; + this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); + this.$block = $block; + this.$selectbox = $dropdown.closest('.selectbox'); + this.$value = $block.find('.value'); + this.$valueContent = $block.find('.value-content'); + this.$sidebarValue = $('.js-due-date-sidebar-value', $block); + this.fieldName = $dropdown.data('field-name'), + this.abilityName = $dropdown.data('ability-name'), + this.issueUpdateURL = $dropdown.data('issue-update') + + this.rawSelectedDate = null; + this.displayedDate = null; + this.datePayload = null; + + this.initGlDropdown(); + this.initRemoveDueDate(); + this.initDatePicker(); + this.initStopPropagation(); + } + + initGlDropdown() { + this.$dropdown.glDropdown({ + hidden: () => { + this.$selectbox.hide(); + this.$value.css('display', ''); + } + }); + } + + initDatePicker() { + this.$datePicker.datepicker({ + dateFormat: 'yy-mm-dd', + defaultDate: $("input[name='" + this.fieldName + "']").val(), + altField: "input[name='" + this.fieldName + "']", + onSelect: () => { + return this.saveDueDate(true); + } + }); + } + + initRemoveDueDate() { + this.$block.on('click', '.js-remove-due-date', (e) => { + e.preventDefault(); + $("input[name='" + this.fieldName + "']").val(''); + return this.saveDueDate(false); + }); + } + + initStopPropagation() { + $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => { + return e.stopImmediatePropagation(); + }); + } + + saveDueDate(isDropdown) { + this.parseSelectedDate(); + this.prepSelectedDate(); + this.submitSelectedDate(isDropdown); + } + + parseSelectedDate() { + this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val(); + if (this.rawSelectedDate.length) { + let dateObj = new Date(this.rawSelectedDate); + this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj); + } else { + this.displayedDate = 'No due date'; + } + } + + prepSelectedDate() { + const datePayload = {}; + datePayload[this.abilityName] = {}; + datePayload[this.abilityName].due_date = this.rawSelectedDate; + this.datePayload = datePayload; + } + + submitSelectedDate(isDropdown) { + return $.ajax({ + type: 'PUT', + url: this.issueUpdateURL, + data: this.datePayload, + dataType: 'json', + beforeSend: () => { + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + + this.$loading.fadeIn(); + + if (isDropdown) { + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + } + + this.$value.css('display', ''); + this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); + this.$sidebarValue.html(this.displayedDate); + + return selectedDateValue.length ? + $('.js-remove-due-date-holder').removeClass('hidden') : + $('.js-remove-due-date-holder').addClass('hidden'); + + } + }).done((data) => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); + } + } + + class DueDateSelectors { + constructor() { + this.initMilestoneDueDate(); + this.initIssuableSelect(); + } + + initMilestoneDueDate() { + const $datePicker = $('.datepicker'); + + if ($datePicker.length) { + const $dueDate = $('#milestone_due_date'); + $datePicker.datepicker({ + dateFormat: 'yy-mm-dd', + onSelect: (dateText, inst) => { + $dueDate.val(dateText); + } + }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())); + } + $('.js-clear-due-date').on('click', (e) => { + e.preventDefault(); + $.datepicker._clearDate($datePicker); + }); + } + + initIssuableSelect() { + const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); + + $('.js-due-date-select').each((i, dropdown) => { + const $dropdown = $(dropdown); + new DueDateSelect({ + $dropdown, + $loading + }); + }); + } + } + + global.DueDateSelectors = DueDateSelectors; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6 deleted file mode 100644 index 13ee794ba38523ffd1b28fad30ad15126017d3bb..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/merge_conflict_data_provider.js.es6 +++ /dev/null @@ -1,347 +0,0 @@ -const HEAD_HEADER_TEXT = 'HEAD//our changes'; -const ORIGIN_HEADER_TEXT = 'origin//their changes'; -const HEAD_BUTTON_TITLE = 'Use ours'; -const ORIGIN_BUTTON_TITLE = 'Use theirs'; - - -class MergeConflictDataProvider { - - getInitialData() { - // TODO: remove reliance on jQuery and DOM state introspection - const diffViewType = $.cookie('diff_view'); - const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited'); - - return { - isLoading : true, - hasError : false, - isParallel : diffViewType === 'parallel', - diffViewType : diffViewType, - fixedLayout : fixedLayout, - isSubmitting : false, - conflictsData : {}, - resolutionData : {} - } - } - - - decorateData(vueInstance, data) { - this.vueInstance = vueInstance; - - if (data.type === 'error') { - vueInstance.hasError = true; - data.errorMessage = data.message; - } - else { - data.shortCommitSha = data.commit_sha.slice(0, 7); - data.commitMessage = data.commit_message; - - this.setParallelLines(data); - this.setInlineLines(data); - this.updateResolutionsData(data); - } - - vueInstance.conflictsData = data; - vueInstance.isSubmitting = false; - - const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict'; - vueInstance.conflictsData.conflictsText = conflictsText; - } - - - updateResolutionsData(data) { - const vi = this.vueInstance; - - data.files.forEach( (file) => { - file.sections.forEach( (section) => { - if (section.conflict) { - vi.$set(`resolutionData['${section.id}']`, false); - } - }); - }); - } - - - setParallelLines(data) { - data.files.forEach( (file) => { - file.filePath = this.getFilePath(file); - file.iconClass = `fa-${file.blob_icon}`; - file.blobPath = file.blob_path; - file.parallelLines = []; - const linesObj = { left: [], right: [] }; - - file.sections.forEach( (section) => { - const { conflict, lines, id } = section; - - if (conflict) { - linesObj.left.push(this.getOriginHeaderLine(id)); - linesObj.right.push(this.getHeadHeaderLine(id)); - } - - lines.forEach( (line) => { - const { type } = line; - - if (conflict) { - if (type === 'old') { - linesObj.left.push(this.getLineForParallelView(line, id, 'conflict')); - } - else if (type === 'new') { - linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true)); - } - } - else { - const lineType = type || 'context'; - - linesObj.left.push (this.getLineForParallelView(line, id, lineType)); - linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); - } - }); - - this.checkLineLengths(linesObj); - }); - - for (let i = 0, len = linesObj.left.length; i < len; i++) { - file.parallelLines.push([ - linesObj.right[i], - linesObj.left[i] - ]); - } - - }); - } - - - checkLineLengths(linesObj) { - let { left, right } = linesObj; - - if (left.length !== right.length) { - if (left.length > right.length) { - const diff = left.length - right.length; - for (let i = 0; i < diff; i++) { - right.push({ lineType: 'emptyLine', richText: '' }); - } - } - else { - const diff = right.length - left.length; - for (let i = 0; i < diff; i++) { - left.push({ lineType: 'emptyLine', richText: '' }); - } - } - } - } - - - setInlineLines(data) { - data.files.forEach( (file) => { - file.iconClass = `fa-${file.blob_icon}`; - file.blobPath = file.blob_path; - file.filePath = this.getFilePath(file); - file.inlineLines = [] - - file.sections.forEach( (section) => { - let currentLineType = 'new'; - const { conflict, lines, id } = section; - - if (conflict) { - file.inlineLines.push(this.getHeadHeaderLine(id)); - } - - lines.forEach( (line) => { - const { type } = line; - - if ((type === 'new' || type === 'old') && currentLineType !== type) { - currentLineType = type; - file.inlineLines.push({ lineType: 'emptyLine', richText: '' }); - } - - this.decorateLineForInlineView(line, id, conflict); - file.inlineLines.push(line); - }) - - if (conflict) { - file.inlineLines.push(this.getOriginHeaderLine(id)); - } - }); - }); - } - - - handleSelected(sectionId, selection) { - const vi = this.vueInstance; - - vi.resolutionData[sectionId] = selection; - vi.conflictsData.files.forEach( (file) => { - file.inlineLines.forEach( (line) => { - if (line.id === sectionId && (line.hasConflict || line.isHeader)) { - this.markLine(line, selection); - } - }); - - file.parallelLines.forEach( (lines) => { - const left = lines[0]; - const right = lines[1]; - const hasSameId = right.id === sectionId || left.id === sectionId; - const isLeftMatch = left.hasConflict || left.isHeader; - const isRightMatch = right.hasConflict || right.isHeader; - - if (hasSameId && (isLeftMatch || isRightMatch)) { - this.markLine(left, selection); - this.markLine(right, selection); - } - }) - }); - } - - - updateViewType(newType) { - const vi = this.vueInstance; - - if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) { - return; - } - - vi.diffViewType = newType; - vi.isParallel = newType === 'parallel'; - $.cookie('diff_view', newType, { - path: (gon && gon.relative_url_root) || '/' - }); - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', !vi.isParallel && vi.fixedLayout); - } - - - markLine(line, selection) { - if (selection === 'head' && line.isHead) { - line.isSelected = true; - line.isUnselected = false; - } - else if (selection === 'origin' && line.isOrigin) { - line.isSelected = true; - line.isUnselected = false; - } - else { - line.isSelected = false; - line.isUnselected = true; - } - } - - - getConflictsCount() { - return Object.keys(this.vueInstance.resolutionData).length; - } - - - getResolvedCount() { - let count = 0; - const data = this.vueInstance.resolutionData; - - for (const id in data) { - const resolution = data[id]; - if (resolution) { - count++; - } - } - - return count; - } - - - isReadyToCommit() { - const { conflictsData, isSubmitting } = this.vueInstance - const allResolved = this.getConflictsCount() === this.getResolvedCount(); - const hasCommitMessage = $.trim(conflictsData.commitMessage).length; - - return !isSubmitting && hasCommitMessage && allResolved; - } - - - getCommitButtonText() { - const initial = 'Commit conflict resolution'; - const inProgress = 'Committing...'; - const vue = this.vueInstance; - - return vue ? vue.isSubmitting ? inProgress : initial : initial; - } - - - decorateLineForInlineView(line, id, conflict) { - const { type } = line; - line.id = id; - line.hasConflict = conflict; - line.isHead = type === 'new'; - line.isOrigin = type === 'old'; - line.hasMatch = type === 'match'; - line.richText = line.rich_text; - line.isSelected = false; - line.isUnselected = false; - } - - getLineForParallelView(line, id, lineType, isHead) { - const { old_line, new_line, rich_text } = line; - const hasConflict = lineType === 'conflict'; - - return { - id, - lineType, - hasConflict, - isHead : hasConflict && isHead, - isOrigin : hasConflict && !isHead, - hasMatch : lineType === 'match', - lineNumber : isHead ? new_line : old_line, - section : isHead ? 'head' : 'origin', - richText : rich_text, - isSelected : false, - isUnselected : false - } - } - - - getHeadHeaderLine(id) { - return { - id : id, - richText : HEAD_HEADER_TEXT, - buttonTitle : HEAD_BUTTON_TITLE, - type : 'new', - section : 'head', - isHeader : true, - isHead : true, - isSelected : false, - isUnselected: false - } - } - - - getOriginHeaderLine(id) { - return { - id : id, - richText : ORIGIN_HEADER_TEXT, - buttonTitle : ORIGIN_BUTTON_TITLE, - type : 'old', - section : 'origin', - isHeader : true, - isOrigin : true, - isSelected : false, - isUnselected: false - } - } - - - handleFailedRequest(vueInstance, data) { - vueInstance.hasError = true; - vueInstance.conflictsData.errorMessage = 'Something went wrong!'; - } - - - getCommitData() { - return { - commit_message: this.vueInstance.conflictsData.commitMessage, - sections: this.vueInstance.resolutionData - } - } - - - getFilePath(file) { - const { old_path, new_path } = file; - return old_path === new_path ? new_path : `${old_path} → ${new_path}`; - } - -} diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6 deleted file mode 100644 index 7e756433bf5fb5687239a3be1f44e0fd4ea78f73..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/merge_conflict_resolver.js.es6 +++ /dev/null @@ -1,82 +0,0 @@ -//= require vue - -class MergeConflictResolver { - - constructor() { - this.dataProvider = new MergeConflictDataProvider() - this.initVue() - } - - - initVue() { - const that = this; - this.vue = new Vue({ - el : '#conflicts', - name : 'MergeConflictResolver', - data : this.dataProvider.getInitialData(), - created : this.fetchData(), - computed : this.setComputedProperties(), - methods : { - handleSelected(sectionId, selection) { - that.dataProvider.handleSelected(sectionId, selection); - }, - handleViewTypeChange(newType) { - that.dataProvider.updateViewType(newType); - }, - commit() { - that.commit(); - } - } - }) - } - - - setComputedProperties() { - const dp = this.dataProvider; - - return { - conflictsCount() { return dp.getConflictsCount() }, - resolvedCount() { return dp.getResolvedCount() }, - readyToCommit() { return dp.isReadyToCommit() }, - commitButtonText() { return dp.getCommitButtonText() } - } - } - - - fetchData() { - const dp = this.dataProvider; - - $.get($('#conflicts').data('conflictsPath')) - .done((data) => { - dp.decorateData(this.vue, data); - }) - .error((data) => { - dp.handleFailedRequest(this.vue, data); - }) - .always(() => { - this.vue.isLoading = false; - - this.vue.$nextTick(() => { - $('#conflicts .js-syntax-highlight').syntaxHighlight(); - }); - - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout); - }) - } - - - commit() { - this.vue.isSubmitting = true; - - $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData()) - .done((data) => { - window.location.href = data.redirect_to; - }) - .error(() => { - this.vue.isSubmitting = false; - new Flash('Something went wrong!'); - }); - } - -} diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..5012bdfe9979fd671ec32f96bf55c70d3ea885f0 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -0,0 +1,93 @@ +((global) => { + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.diffFileEditor = Vue.extend({ + props: { + file: Object, + onCancelDiscardConfirmation: Function, + onAcceptDiscardConfirmation: Function + }, + data() { + return { + saved: false, + loading: false, + fileLoaded: false, + originalContent: '', + } + }, + computed: { + classObject() { + return { + 'saved': this.saved, + 'is-loading': this.loading + }; + } + }, + watch: { + ['file.showEditor'](val) { + this.resetEditorContent(); + + if (!val || this.fileLoaded || this.loading) { + return; + } + + this.loadEditor(); + } + }, + ready() { + if (this.file.loadEditor) { + this.loadEditor(); + } + }, + methods: { + loadEditor() { + this.loading = true; + + $.get(this.file.content_path) + .done((file) => { + let content = this.$el.querySelector('pre'); + let fileContent = document.createTextNode(file.content); + + content.textContent = fileContent.textContent; + + this.originalContent = file.content; + this.fileLoaded = true; + this.editor = ace.edit(content); + this.editor.$blockScrolling = Infinity; // Turn off annoying warning + this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`); + this.editor.on('change', () => { + this.saveDiffResolution(); + }); + this.saveDiffResolution(); + }) + .fail(() => { + new Flash('Failed to load the file, please try again.'); + }) + .always(() => { + this.loading = false; + }); + }, + saveDiffResolution() { + this.saved = true; + + // This probably be better placed in the data provider + this.file.content = this.editor.getValue(); + this.file.resolveEditChanged = this.file.content !== this.originalContent; + this.file.promptDiscardConfirmation = false; + }, + resetEditorContent() { + if (this.fileLoaded) { + this.editor.setValue(this.originalContent, -1); + } + }, + cancelDiscardConfirmation(file) { + this.onCancelDiscardConfirmation(file); + }, + acceptDiscardConfirmation(file) { + this.onAcceptDiscardConfirmation(file); + } + } + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..b4be1c8988d0c6ce37548ede8d2329ed9092cc99 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 @@ -0,0 +1,12 @@ +((global) => { + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.inlineConflictLines = Vue.extend({ + props: { + file: Object + }, + mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..8b0a8ab20731215a5f521887d1b6fa6cbb9115de --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 @@ -0,0 +1,14 @@ +((global) => { + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.parallelConflictLine = Vue.extend({ + props: { + file: Object, + line: Object + }, + mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], + template: '#parallel-conflict-line' + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..eb4cc6a9dac9686480f3dd35fe19405d43cd4858 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 @@ -0,0 +1,15 @@ +((global) => { + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.parallelConflictLines = Vue.extend({ + props: { + file: Object + }, + mixins: [global.mergeConflicts.utils], + components: { + 'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine + } + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..da2fb8b1323657930e7de069248ca7bcc1ad74cd --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 @@ -0,0 +1,30 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + class mergeConflictsService { + constructor(options) { + this.conflictsPath = options.conflictsPath; + this.resolveConflictsPath = options.resolveConflictsPath; + } + + fetchConflictsData() { + return $.ajax({ + dataType: 'json', + url: this.conflictsPath + }); + } + + submitResolveConflicts(data) { + return $.ajax({ + url: this.resolveConflictsPath, + data: JSON.stringify(data), + contentType: 'application/json', + dataType: 'json', + method: 'POST' + }); + } + }; + + global.mergeConflicts.mergeConflictsService = mergeConflictsService; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..5c5c65f29d4fbb5d7fa17545bbb1a068dc03d3b6 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -0,0 +1,437 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + const diffViewType = $.cookie('diff_view'); + const HEAD_HEADER_TEXT = 'HEAD//our changes'; + const ORIGIN_HEADER_TEXT = 'origin//their changes'; + const HEAD_BUTTON_TITLE = 'Use ours'; + const ORIGIN_BUTTON_TITLE = 'Use theirs'; + const INTERACTIVE_RESOLVE_MODE = 'interactive'; + const EDIT_RESOLVE_MODE = 'edit'; + const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE; + const VIEW_TYPES = { + INLINE: 'inline', + PARALLEL: 'parallel' + }; + const CONFLICT_TYPES = { + TEXT: 'text', + TEXT_EDITOR: 'text-editor' + }; + + global.mergeConflicts.mergeConflictsStore = { + state: { + isLoading: true, + hasError: false, + isSubmitting: false, + isParallel: diffViewType === VIEW_TYPES.PARALLEL, + diffViewType: diffViewType, + conflictsData: {} + }, + + setConflictsData(data) { + this.decorateFiles(data.files); + + this.state.conflictsData = { + files: data.files, + commitMessage: data.commit_message, + sourceBranch: data.source_branch, + targetBranch: data.target_branch, + commitMessage: data.commit_message, + shortCommitSha: data.commit_sha.slice(0, 7), + }; + }, + + decorateFiles(files) { + files.forEach((file) => { + file.content = ''; + file.resolutionData = {}; + file.promptDiscardConfirmation = false; + file.resolveMode = DEFAULT_RESOLVE_MODE; + file.filePath = this.getFilePath(file); + file.iconClass = `fa-${file.blob_icon}`; + file.blobPath = file.blob_path; + + if (file.type === CONFLICT_TYPES.TEXT) { + file.showEditor = false; + file.loadEditor = false; + + this.setInlineLine(file); + this.setParallelLine(file); + } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) { + file.showEditor = true; + file.loadEditor = true; + } + }); + }, + + setInlineLine(file) { + file.inlineLines = []; + + file.sections.forEach((section) => { + let currentLineType = 'new'; + const { conflict, lines, id } = section; + + if (conflict) { + file.inlineLines.push(this.getHeadHeaderLine(id)); + } + + lines.forEach((line) => { + const { type } = line; + + if ((type === 'new' || type === 'old') && currentLineType !== type) { + currentLineType = type; + file.inlineLines.push({ lineType: 'emptyLine', richText: '' }); + } + + this.decorateLineForInlineView(line, id, conflict); + file.inlineLines.push(line); + }) + + if (conflict) { + file.inlineLines.push(this.getOriginHeaderLine(id)); + } + }); + }, + + setParallelLine(file) { + file.parallelLines = []; + const linesObj = { left: [], right: [] }; + + file.sections.forEach((section) => { + const { conflict, lines, id } = section; + + if (conflict) { + linesObj.left.push(this.getOriginHeaderLine(id)); + linesObj.right.push(this.getHeadHeaderLine(id)); + } + + lines.forEach((line) => { + const { type } = line; + + if (conflict) { + if (type === 'old') { + linesObj.left.push(this.getLineForParallelView(line, id, 'conflict')); + } else if (type === 'new') { + linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true)); + } + } else { + const lineType = type || 'context'; + + linesObj.left.push (this.getLineForParallelView(line, id, lineType)); + linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); + } + }); + + this.checkLineLengths(linesObj); + }); + + for (let i = 0, len = linesObj.left.length; i < len; i++) { + file.parallelLines.push([ + linesObj.right[i], + linesObj.left[i] + ]); + } + }, + + setLoadingState(state) { + this.state.isLoading = state; + }, + + setErrorState(state) { + this.state.hasError = state; + }, + + setFailedRequest(message) { + this.state.hasError = true; + this.state.conflictsData.errorMessage = message; + }, + + getConflictsCount() { + if (!this.state.conflictsData.files.length) { + return 0; + } + + const files = this.state.conflictsData.files; + let count = 0; + + files.forEach((file) => { + if (file.type === CONFLICT_TYPES.TEXT) { + file.sections.forEach((section) => { + if (section.conflict) { + count++; + } + }); + } else { + count++; + } + }); + + return count; + }, + + getConflictsCountText() { + const count = this.getConflictsCount(); + const text = count ? 'conflicts' : 'conflict'; + + return `${count} ${text}`; + }, + + setViewType(viewType) { + this.state.diffView = viewType; + this.state.isParallel = viewType === VIEW_TYPES.PARALLEL; + + $.cookie('diff_view', viewType, { + path: gon.relative_url_root || '/' + }); + }, + + getHeadHeaderLine(id) { + return { + id: id, + richText: HEAD_HEADER_TEXT, + buttonTitle: HEAD_BUTTON_TITLE, + type: 'new', + section: 'head', + isHeader: true, + isHead: true, + isSelected: false, + isUnselected: false + }; + }, + + decorateLineForInlineView(line, id, conflict) { + const { type } = line; + line.id = id; + line.hasConflict = conflict; + line.isHead = type === 'new'; + line.isOrigin = type === 'old'; + line.hasMatch = type === 'match'; + line.richText = line.rich_text; + line.isSelected = false; + line.isUnselected = false; + }, + + getLineForParallelView(line, id, lineType, isHead) { + const { old_line, new_line, rich_text } = line; + const hasConflict = lineType === 'conflict'; + + return { + id, + lineType, + hasConflict, + isHead: hasConflict && isHead, + isOrigin: hasConflict && !isHead, + hasMatch: lineType === 'match', + lineNumber: isHead ? new_line : old_line, + section: isHead ? 'head' : 'origin', + richText: rich_text, + isSelected: false, + isUnselected: false + }; + }, + + getOriginHeaderLine(id) { + return { + id: id, + richText: ORIGIN_HEADER_TEXT, + buttonTitle: ORIGIN_BUTTON_TITLE, + type: 'old', + section: 'origin', + isHeader: true, + isOrigin: true, + isSelected: false, + isUnselected: false + }; + }, + + getFilePath(file) { + const { old_path, new_path } = file; + return old_path === new_path ? new_path : `${old_path} → ${new_path}`; + }, + + checkLineLengths(linesObj) { + let { left, right } = linesObj; + + if (left.length !== right.length) { + if (left.length > right.length) { + const diff = left.length - right.length; + for (let i = 0; i < diff; i++) { + right.push({ lineType: 'emptyLine', richText: '' }); + } + } else { + const diff = right.length - left.length; + for (let i = 0; i < diff; i++) { + left.push({ lineType: 'emptyLine', richText: '' }); + } + } + } + }, + + setPromptConfirmationState(file, state) { + file.promptDiscardConfirmation = state; + }, + + setFileResolveMode(file, mode) { + if (mode === INTERACTIVE_RESOLVE_MODE) { + file.showEditor = false; + } else if (mode === EDIT_RESOLVE_MODE) { + // Restore Interactive mode when switching to Edit mode + file.showEditor = true; + file.loadEditor = true; + file.resolutionData = {}; + + this.restoreFileLinesState(file); + } + + file.resolveMode = mode; + }, + + restoreFileLinesState(file) { + file.inlineLines.forEach((line) => { + if (line.hasConflict || line.isHeader) { + line.isSelected = false; + line.isUnselected = false; + } + }); + + file.parallelLines.forEach((lines) => { + const left = lines[0]; + const right = lines[1]; + const isLeftMatch = left.hasConflict || left.isHeader; + const isRightMatch = right.hasConflict || right.isHeader; + + if (isLeftMatch || isRightMatch) { + left.isSelected = false; + left.isUnselected = false; + right.isSelected = false; + right.isUnselected = false; + } + }); + }, + + isReadyToCommit() { + const files = this.state.conflictsData.files; + const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length; + let unresolved = 0; + + for (let i = 0, l = files.length; i < l; i++) { + let file = files[i]; + + if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) { + let numberConflicts = 0; + let resolvedConflicts = Object.keys(file.resolutionData).length + + // We only check for conflicts type 'text' + // since conflicts `text_editor` can´t be resolved in interactive mode + if (file.type === CONFLICT_TYPES.TEXT) { + for (let j = 0, k = file.sections.length; j < k; j++) { + if (file.sections[j].conflict) { + numberConflicts++; + } + } + + if (resolvedConflicts !== numberConflicts) { + unresolved++; + } + } + } else if (file.resolveMode === EDIT_RESOLVE_MODE) { + + // Unlikely to happen since switching to Edit mode saves content automatically. + // Checking anyway in case the save strategy changes in the future + if (!file.content) { + unresolved++; + continue; + } + } + } + + return !this.state.isSubmitting && hasCommitMessage && !unresolved; + }, + + getCommitButtonText() { + const initial = 'Commit conflict resolution'; + const inProgress = 'Committing...'; + + return this.state ? this.state.isSubmitting ? inProgress : initial : initial; + }, + + getCommitData() { + let commitData = {}; + + commitData = { + commit_message: this.state.conflictsData.commitMessage, + files: [] + }; + + this.state.conflictsData.files.forEach((file) => { + let addFile; + + addFile = { + old_path: file.old_path, + new_path: file.new_path + }; + + if (file.type === CONFLICT_TYPES.TEXT) { + + // Submit only one data for type of editing + if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) { + addFile.sections = file.resolutionData; + } else if (file.resolveMode === EDIT_RESOLVE_MODE) { + addFile.content = file.content; + } + } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) { + addFile.content = file.content; + } + + commitData.files.push(addFile); + }); + + return commitData; + }, + + handleSelected(file, sectionId, selection) { + Vue.set(file.resolutionData, sectionId, selection); + + file.inlineLines.forEach((line) => { + if (line.id === sectionId && (line.hasConflict || line.isHeader)) { + this.markLine(line, selection); + } + }); + + file.parallelLines.forEach((lines) => { + const left = lines[0]; + const right = lines[1]; + const hasSameId = right.id === sectionId || left.id === sectionId; + const isLeftMatch = left.hasConflict || left.isHeader; + const isRightMatch = right.hasConflict || right.isHeader; + + if (hasSameId && (isLeftMatch || isRightMatch)) { + this.markLine(left, selection); + this.markLine(right, selection); + } + }); + }, + + markLine(line, selection) { + if (selection === 'head' && line.isHead) { + line.isSelected = true; + line.isUnselected = false; + } else if (selection === 'origin' && line.isOrigin) { + line.isSelected = true; + line.isUnselected = false; + } else { + line.isSelected = false; + line.isUnselected = true; + } + }, + + setSubmitState(state) { + this.state.isSubmitting = state; + }, + + fileTextTypePresent() { + return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT); + } + }; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..7fd3749b3e21f4a656a800d0c9ae655e277d6431 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -0,0 +1,89 @@ +//= require vue +//= require ./merge_conflict_store +//= require ./merge_conflict_service +//= require ./mixins/line_conflict_utils +//= require ./mixins/line_conflict_actions +//= require ./components/diff_file_editor +//= require ./components/inline_conflict_lines +//= require ./components/parallel_conflict_line +//= require ./components/parallel_conflict_lines + +$(() => { + const INTERACTIVE_RESOLVE_MODE = 'interactive'; + const conflictsEl = document.querySelector('#conflicts'); + const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; + const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({ + conflictsPath: conflictsEl.dataset.conflictsPath, + resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath + }); + + gl.MergeConflictsResolverApp = new Vue({ + el: '#conflicts', + data: mergeConflictsStore.state, + components: { + 'diff-file-editor': gl.mergeConflicts.diffFileEditor, + 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, + 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines + }, + computed: { + conflictsCountText() { return mergeConflictsStore.getConflictsCountText() }, + readyToCommit() { return mergeConflictsStore.isReadyToCommit() }, + commitButtonText() { return mergeConflictsStore.getCommitButtonText() }, + showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() } + }, + created() { + mergeConflictsService + .fetchConflictsData() + .done((data) => { + if (data.type === 'error') { + mergeConflictsStore.setFailedRequest(data.message); + } else { + mergeConflictsStore.setConflictsData(data); + } + }) + .error(() => { + mergeConflictsStore.setFailedRequest(); + }) + .always(() => { + mergeConflictsStore.setLoadingState(false); + + this.$nextTick(() => { + $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight(); + }); + }); + }, + methods: { + handleViewTypeChange(viewType) { + mergeConflictsStore.setViewType(viewType); + }, + onClickResolveModeButton(file, mode) { + if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) { + mergeConflictsStore.setPromptConfirmationState(file, true); + return; + } + + mergeConflictsStore.setFileResolveMode(file, mode); + }, + acceptDiscardConfirmation(file) { + mergeConflictsStore.setPromptConfirmationState(file, false); + mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE); + }, + cancelDiscardConfirmation(file) { + mergeConflictsStore.setPromptConfirmationState(file, false); + }, + commit() { + mergeConflictsStore.setSubmitState(true); + + mergeConflictsService + .submitResolveConflicts(mergeConflictsStore.getCommitData()) + .done((data) => { + window.location.href = data.redirect_to; + }) + .error(() => { + mergeConflictsStore.setSubmitState(false); + new Flash('Failed to save merge conflicts resolutions. Please try again!'); + }); + } + } + }) +}); diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..114a2c5b3055e7890c8244d0ee9bad8817edefb3 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 @@ -0,0 +1,12 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.actions = { + methods: { + handleSelected(file, sectionId, selection) { + gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection); + } + } + }; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 new file mode 100644 index 0000000000000000000000000000000000000000..b846a90ab2afec062292b20463991fcfe0a296d3 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 @@ -0,0 +1,18 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.utils = { + methods: { + lineCssClass(line) { + return { + 'head': line.isHead, + 'origin': line.isOrigin, + 'match': line.hasMatch, + 'selected': line.isSelected, + 'unselected': line.isUnselected + }; + } + } + }; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 6bf63ee6979173f87dabba760db933a083f62698..a7624de6089f17a77eab7e8f2daa3b4116655ef1 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -15,7 +15,7 @@ $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); - graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide') + graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand') } addMarginToBuildColumns() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 8e38ccf7e44ee15d9c4f2d656333b878a654901d..b8347367717f4975d4d52f9b209e22c951038b81 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -7,6 +7,7 @@ function ProjectFindFile(element1, options) { this.element = element1; this.options = options; + this.goToBlob = bind(this.goToBlob, this); this.goToTree = bind(this.goToTree, this); this.selectRowDown = bind(this.selectRowDown, this); this.selectRowUp = bind(this.selectRowUp, this); @@ -154,6 +155,14 @@ return location.href = this.options.treeUrl; }; + ProjectFindFile.prototype.goToBlob = function() { + var $link = this.element.find(".tree-item.selected .tree-item-file-name a"); + + if ($link.length) { + $link.get(0).click(); + } + }; + return ProjectFindFile; })(); diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index 2517f778365001618aac5bb2176637d573b3fc31..bf4b2e320cd5980f04dddeffac6d370f778155fd 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -76,7 +76,7 @@ this.renderState(); return $.ajax({ type: 'GET', - url: `/u/${username}/exists`, + url: `/users/${username}/exists`, dataType: 'json', success: (res) => this.setAvailabilityState(res.exists) }); diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 8df0067fac175ce45f751f44f1d1ab42b0946307..55de9053be581961f058fe493a459ad847dcf434 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -45,40 +45,38 @@ } h1 { - font-size: 2em; + font-size: 1.75em; font-weight: 600; - margin: 1em 0 10px; + margin: 16px 0 10px; padding: 0 0 0.3em; - border-bottom: 1px solid $btn-default-border; + border-bottom: 1px solid $white-dark; color: $gl-gray-dark; } h2 { - font-size: 1.6em; + font-size: 1.5em; font-weight: 600; - margin: 1em 0 10px; - padding-bottom: 0.3em; - border-bottom: 1px solid $btn-default-border; + margin: 16px 0 10px; color: $gl-gray-dark; } h3 { - margin: 1em 0 10px; - font-size: 1.4em; + margin: 16px 0 10px; + font-size: 1.3em; } h4 { - margin: 1em 0 10px; - font-size: 1.25em; + margin: 16px 0 10px; + font-size: 1.2em; } h5 { - margin: 1em 0 10px; + margin: 16px 0 10px; font-size: 1em; } h6 { - margin: 1em 0 10px; + margin: 16px 0 10px; font-size: 0.95em; } @@ -87,7 +85,12 @@ font-size: inherit; padding: 8px 21px; margin: 12px 0; - border-left: 3px solid #e7e9ed; + border-left: 3px solid $white-dark; + } + + blockquote:dir(rtl) { + border-left: 0; + border-right: 3px solid $white-dark; } blockquote p { @@ -112,6 +115,10 @@ } } + table:dir(rtl) th { + text-align: right; + } + pre { margin: 12px 0; font-size: 13px; @@ -129,6 +136,10 @@ margin: 3px 0 3px 28px !important; } + ul:dir(rtl), ol:dir(rtl) { + margin: 3px 28px 3px 0 !important; + } + li { line-height: 1.6em; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4c34ed3ebf78d71f1560151bcbf11db680b9cafe..7690d65de8e86595b4d3c64085dff2498a2c6d56 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -56,6 +56,7 @@ $border-gray-light: #dcdcdc; $border-gray-normal: #d7d7d7; $border-gray-dark: #c6cacf; +$border-green-extra-light: #9adb84; $border-green-light: #2faa60; $border-green-normal: #2ca05b; $border-green-dark: #279654; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 4d9c73c6840a02456836801acc3d3a70c03d0692..2357671c2aeb731c838f5061469f9ad4036f75dd 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -20,9 +20,11 @@ .detail-page-description { .title { - margin: 0; - font-size: 23px; + margin: 0 0 16px; + font-size: 2em; color: $gl-gray-dark; + padding: 0 0 0.3em; + border-bottom: 1px solid $white-dark; } .description { diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 49013d7cac9ada5416537fe26b6e8ebcbc756393..eed2b0ab7ccc961d257d3bddb0be096ebcd89e74 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -237,4 +237,51 @@ $colors: ( .btn-success .fa-spinner { color: #fff; } + + .editor-wrap { + &.is-loading { + .editor { + display: none; + } + + .loading { + display: block; + } + } + + &.saved { + .editor { + border-top: solid 2px $border-green-extra-light; + } + } + + .editor { + pre { + height: 350px; + border: none; + border-radius: 0; + margin-bottom: 0; + } + } + + .loading { + display: none; + } + } + + .discard-changes-alert { + background-color: $background-color; + text-align: right; + padding: $gl-padding-top $gl-padding; + color: $gl-text-color; + + .discard-actions { + display: inline-block; + margin-left: 10px; + } + } + + .resolve-conflicts-form { + padding-top: $gl-padding; + } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index afc4e517fde3f556cfcb8c03118b8131e644e44d..101472278e26f177635d74f2535386ddd9a80374 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -429,13 +429,6 @@ } } -.merge-request-details { - - .title { - margin-bottom: 20px; - } -} - .merge-request-tabs { background-color: #fff; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 99c0f6362d0bd1230a0216066ceadc6c53546f8b..6ea7a2b5498bd08ad5b794fe95e8e21764d757fd 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -169,4 +169,8 @@ margin-top: 11px; position: relative; z-index: 2; + + .download-button { + margin-left: $btn-side-margin; + } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 705824502eb971a698e9df4a050751cda66c61c9..37600ed875c2537653df0f3e85685306096687a8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -118,7 +118,12 @@ class ApplicationController < ActionController::Base end def render_404 - render file: Rails.root.join("public", "404"), layout: false, status: "404" + respond_to do |format| + format.html do + render file: Rails.root.join("public", "404"), layout: false, status: "404" + end + format.any { head :not_found } + end end def no_cache_headers diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9207c954335cb94cc1965b150a2eeebaa25bc850..a39b47b6d9586d7327a6579427c55e7c6671f3f8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check, :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] - before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] + before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs] - before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] + before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :apply_diff_view_cookie!, only: [:new_diffs] before_action :build_merge_request, only: [:new, :new_diffs] @@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :authenticate_user!, only: [:assign_related_issues] - before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] + before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts] def index @merge_requests = merge_requests_collection @@ -170,6 +170,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def conflict_for_path + return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui? + + file = @merge_request.conflicts.file_for_path(params[:old_path], params[:new_path]) + + return render_404 unless file + + render json: file, full_content: true + end + def resolve_conflicts return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui? @@ -184,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } - rescue Gitlab::Conflict::File::MissingResolution => e + rescue Gitlab::Conflict::ResolutionError => e render status: :bad_request, json: { message: e.message } end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5604296cba5c579cab89a9c0efcd705902879ba3..941b01c29e0697027fbccc8e0d72eced440d5819 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -49,6 +49,10 @@ module Ci transition any => :canceled end + # IMPORTANT + # Do not add any operations to this state_machine + # Create a separate worker for each new operation + before_transition [:created, :pending] => :running do |pipeline| pipeline.started_at = Time.now end @@ -62,13 +66,11 @@ module Ci end after_transition [:created, :pending] => :running do |pipeline| - MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). - update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil) + pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } end after_transition any => [:success] do |pipeline| - MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). - update_all(latest_build_finished_at: pipeline.finished_at) + pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) } end after_transition [:created, :pending, :running] => :success do |pipeline| diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5ccfe11a2a29a12e954791a9677febe71074c969..8c6905a442de644369cab82bc85961fd9977200e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -871,7 +871,7 @@ class MergeRequest < ActiveRecord::Base # files. conflicts.files.each(&:lines) @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 - rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing + rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing @conflicts_can_be_resolved_in_ui = false end end diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index 19caa038c4415f3198b565e367173f5bd9832173..d22a1d3e0ad3d2c2f797080c41368d4d70074da9 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,5 +1,8 @@ module MergeRequests class ResolveService < MergeRequests::BaseService + class MissingFiles < Gitlab::Conflict::ResolutionError + end + attr_accessor :conflicts, :rugged, :merge_index, :merge_request def execute(merge_request) @@ -10,8 +13,16 @@ module MergeRequests fetch_their_commit! - conflicts.files.each do |file| - write_resolved_file_to_index(file, params[:sections]) + params[:files].each do |file_params| + conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path]) + + write_resolved_file_to_index(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 = { @@ -23,8 +34,13 @@ module MergeRequests project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params) end - def write_resolved_file_to_index(file, resolutions) - new_file = file.resolve_lines(resolutions).map(&:text).join("\n") + def write_resolved_file_to_index(file, params) + new_file = if params[:sections] + file.resolve_lines(params[:sections]).map(&:text).join("\n") + elsif params[:content] + file.resolve_content(params[:content]) + end + our_path = file.our_path merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 9089586a89d02a7b5d763186837b954ba889a4b8..7e83a88913ad3d42ec16a32916d95f59673bc07d 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,5 +1,5 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - %span{class: 'hidden-xs hidden-sm'} + %span{class: 'hidden-xs hidden-sm download-button'} .dropdown.inline %button.btn{ 'data-toggle' => 'dropdown' } = icon('download') diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index a524936f73cb3179a97516ef01dfb006e7d7991e..d9f74d2cbfbd4c83af628b4c9e418bf9b2de1db2 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,11 +1,7 @@ -- class_bindings = "{ | - 'head': line.isHead, | - 'origin': line.isOrigin, | - 'match': line.hasMatch, | - 'selected': line.isSelected, | - 'unselected': line.isUnselected }" - - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') + = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" .merge-request-details.issuable-details @@ -24,6 +20,21 @@ = render partial: "projects/merge_requests/conflicts/commit_stats" .files-wrapper{"v-if" => "!isLoading && !hasError"} - = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings } - = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings } + .files + .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"} + .file-title + %i.fa.fa-fw{":class" => "file.iconClass"} + %strong {{file.filePath}} + = render partial: 'projects/merge_requests/conflicts/file_actions' + .diff-content.diff-wrap-lines + .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines" + .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines" + %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"} + = render partial: "projects/merge_requests/conflicts/components/diff_file_editor" + = render partial: "projects/merge_requests/conflicts/submit_form" + +-# Components += render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line' diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index 457c467fba9825e161cccbad4fd94686b241eb7c..5ab3cd96163c3d623fe704ae406c616a813d3fdb 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -1,20 +1,16 @@ .content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"} - .inline-parallel-buttons + .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"} .btn-group - %a.btn{ | - ":class" => "{'active': !isParallel}", | - "@click" => "handleViewTypeChange('inline')"} + %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"} Inline - %a.btn{ | - ":class" => "{'active': isParallel}", | - "@click" => "handleViewTypeChange('parallel')"} + %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"} Side-by-side .js-toggle-container .commit-stat-summary Showing - %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}} + %strong.cred {{conflictsCountText}} between - %strong {{conflictsData.source_branch}} + %strong {{conflictsData.sourceBranch}} and - %strong {{conflictsData.target_branch}} + %strong {{conflictsData.targetBranch}} diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..05af57acf038bfedf4a491024c78fe581218ad6b --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -0,0 +1,12 @@ +.file-actions + .btn-group{"v-if" => "file.type === 'text'"} + %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }", + '@click' => "onClickResolveModeButton(file, 'interactive')", + type: 'button' } + Interactive mode + %button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }", + '@click' => "onClickResolveModeButton(file, 'edit')", + type: 'button' } + Edit inline + %a.btn.view-file.btn-file-option{":href" => "file.blobPath"} + View file @{{conflictsData.shortCommitSha}} diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml deleted file mode 100644 index 19c7da4b5e39a6a9c1ccbfd2151b76e88ef5e286..0000000000000000000000000000000000000000 --- a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -.files{"v-show" => "!isParallel"} - .diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"} - .file-title - %i.fa.fa-fw{":class" => "file.iconClass"} - %strong {{file.filePath}} - .file-actions - %a.btn.view-file.btn-file-option{":href" => "file.blobPath"} - View file @{{conflictsData.shortCommitSha}} - - .diff-content.diff-wrap-lines - .diff-wrap-lines.code.file-content.js-syntax-highlight - %table - %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} - %template{"v-if" => "!line.isHeader"} - %td.diff-line-num.new_line{":class" => class_bindings} - %a {{line.new_line}} - %td.diff-line-num.old_line{":class" => class_bindings} - %a {{line.old_line}} - %td.line_content{":class" => class_bindings} - {{{line.richText}}} - - %template{"v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => class_bindings} - %td.diff-line-num.header{":class" => class_bindings} - %td.line_content.header{":class" => class_bindings} - %strong {{{line.richText}}} - %button.btn{"@click" => "handleSelected(line.id, line.section)"} - {{line.buttonTitle}} diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml deleted file mode 100644 index 2e6f67c2eaf867c0d5aa94e255fd0dfb5458c1e1..0000000000000000000000000000000000000000 --- a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.files{"v-show" => "isParallel"} - .diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"} - .file-title - %i.fa.fa-fw{":class" => "file.iconClass"} - %strong {{file.filePath}} - .file-actions - %a.btn.view-file.btn-file-option{":href" => "file.blobPath"} - View file @{{conflictsData.shortCommitSha}} - - .diff-content.diff-wrap-lines - .diff-wrap-lines.code.file-content.js-syntax-highlight - %table - %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"} - %template{"v-for" => "line in section"} - - %template{"v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => class_bindings} - %td.line_content.header{":class" => class_bindings} - %strong {{line.richText}} - %button.btn{"@click" => "handleSelected(line.id, line.section)"} - {{line.buttonTitle}} - - %template{"v-if" => "!line.isHeader"} - %td.diff-line-num.old_line{":class" => class_bindings} - {{line.lineNumber}} - %td.line_content.parallel{":class" => class_bindings} - {{{line.richText}}} diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml index 78bd4133ea292416bf3b683b3bce89220be47b28..6ffaa9ad4d226dce4c99c3722fec9cb1a74a717b 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -1,15 +1,16 @@ -.content-block.oneline-block.files-changed - %strong.resolved-count {{resolvedCount}} - of - %strong.total-count {{conflictsCount}} - conflicts have been resolved - - .commit-message-container.form-group - .max-width-marker - %textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"} - {{{conflictsData.commitMessage}}} - - %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"} - %span {{commitButtonText}} - - = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel" +.form-horizontal.resolve-conflicts-form + .form-group + %label.col-sm-2.control-label{ "for" => "commit-message" } + Commit message + .col-sm-10 + .commit-message-container + .max-width-marker + %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" } + .form-group + .col-sm-offset-2.col-sm-10 + .row + .col-xs-6 + %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" } + %span {{commitButtonText}} + .col-xs-6.text-right + = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel" diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..3c927d362c28612641a7cc4bda453544b861dde2 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml @@ -0,0 +1,13 @@ +%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"} + .diff-editor-wrap{ "v-show" => "file.showEditor" } + .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } + .discard-changes-alert + Are you sure you want to discard your changes? + .discard-actions + %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes + %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel + .editor-wrap{ ":class" => "classObject" } + .loading + %i.fa.fa-spinner.fa-spin + .editor + %pre{ "style" => "height: 350px" } diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..f094df7fcaa921e182f09e60771ad447f1c18263 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml @@ -0,0 +1,15 @@ +%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"} + %table + %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} + %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %a {{line.new_line}} + %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %a {{line.old_line}} + %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + {{{line.richText}}} + %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %strong {{{line.richText}}} + %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } + {{line.buttonTitle}} diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..5690bf7419cc1f8899735c2c81aa596d29668e85 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml @@ -0,0 +1,10 @@ +%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"} + %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %strong {{line.richText}} + %button.btn{"@click" => "handleSelected(file, line.id, line.section)"} + {{line.buttonTitle}} + %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + {{line.lineNumber}} + %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + {{{line.richText}}} diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..a8ecdf593934dd7af4c3ec2b346e8656bd832e19 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml @@ -0,0 +1,4 @@ +%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"} + %table + %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"} + %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"} diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index f805998803888c27c639424ef5a4fa465a083ac7..ba9f0c276612eb03d94962d629c68c5ec9e4ee8d 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -171,5 +171,5 @@ new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') - new DueDateSelect(); + new gl.DueDateSelectors(); sidebar = new Sidebar(); diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..7bb92df3bbd49738891ea3f27e5693cb7040e1a6 --- /dev/null +++ b/app/workers/pipeline_metrics_worker.rb @@ -0,0 +1,30 @@ +class PipelineMetricsWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(pipeline_id) + Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| + update_metrics_for_active_pipeline(pipeline) if pipeline.active? + update_metrics_for_succeeded_pipeline(pipeline) if pipeline.success? + end + end + + private + + def update_metrics_for_active_pipeline(pipeline) + metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil) + end + + def update_metrics_for_succeeded_pipeline(pipeline) + metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at) + end + + def metrics(pipeline) + MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline)) + end + + def merge_requests(pipeline) + pipeline.merge_requests.map(&:id) + end +end diff --git a/config/application.rb b/config/application.rb index 962ffe0708d0de97096b632a02ffed50f0e32fe2..8a9c539cb4375774057ec431908e239c0f27b041 100644 --- a/config/application.rb +++ b/config/application.rb @@ -89,6 +89,7 @@ module Gitlab config.assets.precompile << "profile/profile_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" + config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index be22085b0df75c54e43b09ebea328a7cce4c85af..3b8771543e4fb5f1a1500cb1071924aca5f8388b 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled? ['app', 'finders'] => ['app', 'finders'], ['app', 'mailers', 'emails'] => ['app', 'mailers'], ['app', 'services', '**'] => ['app', 'services'], + ['lib', 'gitlab', 'conflicts'] => ['lib'], ['lib', 'gitlab', 'diff'] => ['lib'], ['lib', 'gitlab', 'email', 'message'] => ['lib'], ['lib', 'gitlab', 'checks'] => ['lib'] diff --git a/config/routes/project.rb b/config/routes/project.rb index 2cd8c60794a1a03c2ec2927f29ca1b4b51e305a9..711a59df74424d66ce37e331a1b30b8ec294d0d0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -267,6 +267,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: get :commits get :diffs get :conflicts + get :conflict_for_path get :builds get :pipelines get :merge_check diff --git a/config/routes/user.rb b/config/routes/user.rb index dfb5d2a2ba4d4cae868493f92200c6bfed2d701a..0a9c924863da8d86f6e46bd5cc95146856bd8e6c 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -1,8 +1,5 @@ require 'constraints/user_url_constrainer' -get '/u/:username', to: redirect('/%{username}'), - constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } - devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations, passwords: :passwords, @@ -23,7 +20,7 @@ constraints(UserUrlConstrainer.new) do end end -scope(path: 'u/:username', +scope(path: 'users/:username', as: :user, constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, controller: :users) do @@ -36,3 +33,12 @@ scope(path: 'u/:username', get :exists get '/', to: redirect('/%{username}') end + +# Compatibility with old routing +# TODO (dzaporozhets): remove in 10.0 +get '/u/:username', to: redirect('/%{username}'), constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } +# TODO (dzaporozhets): remove in 9.0 +get '/u/:username/groups', to: redirect('/users/%{username}/groups'), constraints: { username: /[a-zA-Z.0-9_\-]+/ } +get '/u/:username/projects', to: redirect('/users/%{username}/projects'), constraints: { username: /[a-zA-Z.0-9_\-]+/ } +get '/u/:username/snippets', to: redirect('/users/%{username}/snippets'), constraints: { username: /[a-zA-Z.0-9_\-]+/ } +get '/u/:username/contributed', to: redirect('/users/%{username}/contributed'), constraints: { username: /[a-zA-Z.0-9_\-]+/ } diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index c464e3f3f71261c622ca1d7908e29850dffc3406..06111f4ab671a4d73c7f6878500b4395a2793653 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -43,7 +43,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/root" + "web_url": "http://gitlab.example.com/root" }, "created_at": "2016-06-15T10:09:34.206Z", "updated_at": "2016-06-15T10:09:34.206Z", @@ -59,7 +59,7 @@ Example Response: "id": 26, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/user4" + "web_url": "http://gitlab.example.com/user4" }, "created_at": "2016-06-15T10:09:34.177Z", "updated_at": "2016-06-15T10:09:34.177Z", @@ -103,7 +103,7 @@ Example Response: "id": 26, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/user4" + "web_url": "http://gitlab.example.com/user4" }, "created_at": "2016-06-15T10:09:34.177Z", "updated_at": "2016-06-15T10:09:34.177Z", @@ -146,7 +146,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/root" + "web_url": "http://gitlab.example.com/root" }, "created_at": "2016-06-17T17:47:29.266Z", "updated_at": "2016-06-17T17:47:29.266Z", @@ -190,7 +190,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/root" + "web_url": "http://gitlab.example.com/root" }, "created_at": "2016-06-17T17:47:29.266Z", "updated_at": "2016-06-17T17:47:29.266Z", @@ -238,7 +238,7 @@ Example Response: "id": 26, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/user4" + "web_url": "http://gitlab.example.com/user4" }, "created_at": "2016-06-15T10:09:34.197Z", "updated_at": "2016-06-15T10:09:34.197Z", @@ -279,7 +279,7 @@ Example Response: "id": 26, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/user4" + "web_url": "http://gitlab.example.com/user4" }, "created_at": "2016-06-15T10:09:34.197Z", "updated_at": "2016-06-15T10:09:34.197Z", @@ -319,7 +319,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/root" + "web_url": "http://gitlab.example.com/root" }, "created_at": "2016-06-17T19:59:55.888Z", "updated_at": "2016-06-17T19:59:55.888Z", @@ -362,7 +362,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://gitlab.example.com/u/root" + "web_url": "http://gitlab.example.com/root" }, "created_at": "2016-06-17T19:59:55.888Z", "updated_at": "2016-06-17T19:59:55.888Z", diff --git a/doc/api/builds.md b/doc/api/builds.md index e8a9e4743d39e7de64511c8dafa6ded7c1d0e3b4..e40f198696daaeed832c7042d97fdf0bf7d9d9c4 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -64,7 +64,7 @@ Example of response "state": "active", "twitter": "", "username": "root", - "web_url": "http://gitlab.dev/u/root", + "web_url": "http://gitlab.dev/root", "website_url": "" } }, @@ -108,7 +108,7 @@ Example of response "state": "active", "twitter": "", "username": "root", - "web_url": "http://gitlab.dev/u/root", + "web_url": "http://gitlab.dev/root", "website_url": "" } } @@ -212,7 +212,7 @@ Example of response "state": "active", "twitter": "", "username": "root", - "web_url": "http://gitlab.dev/u/root", + "web_url": "http://gitlab.dev/root", "website_url": "" } } @@ -279,7 +279,7 @@ Example of response "state": "active", "twitter": "", "username": "root", - "web_url": "http://gitlab.dev/u/root", + "web_url": "http://gitlab.dev/root", "website_url": "" } } diff --git a/doc/api/commits.md b/doc/api/commits.md index 3e20beefb8a1872eaa3dd50eb5918a5fb54eb35a..6e0882a94dea7ab5793230b75cfe28d8e3d28c9b 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -288,7 +288,7 @@ Example response: ```json { "author" : { - "web_url" : "https://gitlab.example.com/u/thedude", + "web_url" : "https://gitlab.example.com/thedude", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "username" : "thedude", "state" : "active", @@ -343,7 +343,7 @@ Example response: "author" : { "username" : "thedude", "state" : "active", - "web_url" : "https://gitlab.example.com/u/thedude", + "web_url" : "https://gitlab.example.com/thedude", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "id" : 28, "name" : "Jeff Lebowski" @@ -370,7 +370,7 @@ Example response: "id" : 28, "name" : "Jeff Lebowski", "username" : "thedude", - "web_url" : "https://gitlab.example.com/u/thedude", + "web_url" : "https://gitlab.example.com/thedude", "state" : "active", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png" }, @@ -408,7 +408,7 @@ Example response: ```json { "author" : { - "web_url" : "https://gitlab.example.com/u/thedude", + "web_url" : "https://gitlab.example.com/thedude", "name" : "Jeff Lebowski", "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png", "username" : "thedude", diff --git a/doc/api/deployments.md b/doc/api/deployments.md index 417962de82d42e1d7a0c99523692d1f98d12c222..3d95c4cde604add0089bddf52f8f5bab306ca983 100644 --- a/doc/api/deployments.md +++ b/doc/api/deployments.md @@ -56,7 +56,7 @@ Example of response "state": "active", "twitter": "", "username": "root", - "web_url": "http://localhost:3000/u/root", + "web_url": "http://localhost:3000/root", "website_url": "" } }, @@ -75,7 +75,7 @@ Example of response "name": "Administrator", "state": "active", "username": "root", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" } }, { @@ -114,7 +114,7 @@ Example of response "state": "active", "twitter": "", "username": "root", - "web_url": "http://localhost:3000/u/root", + "web_url": "http://localhost:3000/root", "website_url": "" } }, @@ -133,7 +133,7 @@ Example of response "name": "Administrator", "state": "active", "username": "root", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" } } ] @@ -169,7 +169,7 @@ Example of response "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "environment": { "id": 9, @@ -193,7 +193,7 @@ Example of response "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/u/root", + "web_url": "http://localhost:3000/root", "created_at": "2016-08-11T07:09:20.351Z", "is_admin": true, "bio": null, diff --git a/doc/api/issues.md b/doc/api/issues.md index eed0d2fce5172a62aaa983a21847880e34036068..134263d27b4b1187621e9658b843255d5334288b 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -46,7 +46,7 @@ Example response: "author" : { "state" : "active", "id" : 18, - "web_url" : "https://gitlab.example.com/u/eileen.lowe", + "web_url" : "https://gitlab.example.com/eileen.lowe", "name" : "Alexandra Bashirian", "avatar_url" : null, "username" : "eileen.lowe" @@ -67,7 +67,7 @@ Example response: "state" : "active", "id" : 1, "name" : "Administrator", - "web_url" : "https://gitlab.example.com/u/root", + "web_url" : "https://gitlab.example.com/root", "avatar_url" : null, "username" : "root" }, @@ -134,7 +134,7 @@ Example response: }, "author" : { "state" : "active", - "web_url" : "https://gitlab.example.com/u/root", + "web_url" : "https://gitlab.example.com/root", "avatar_url" : null, "username" : "root", "id" : 1, @@ -145,7 +145,7 @@ Example response: "iid" : 1, "assignee" : { "avatar_url" : null, - "web_url" : "https://gitlab.example.com/u/lennie", + "web_url" : "https://gitlab.example.com/lennie", "state" : "active", "username" : "lennie", "id" : 9, @@ -215,7 +215,7 @@ Example response: }, "author" : { "state" : "active", - "web_url" : "https://gitlab.example.com/u/root", + "web_url" : "https://gitlab.example.com/root", "avatar_url" : null, "username" : "root", "id" : 1, @@ -226,7 +226,7 @@ Example response: "iid" : 1, "assignee" : { "avatar_url" : null, - "web_url" : "https://gitlab.example.com/u/lennie", + "web_url" : "https://gitlab.example.com/lennie", "state" : "active", "username" : "lennie", "id" : 9, @@ -281,7 +281,7 @@ Example response: }, "author" : { "state" : "active", - "web_url" : "https://gitlab.example.com/u/root", + "web_url" : "https://gitlab.example.com/root", "avatar_url" : null, "username" : "root", "id" : 1, @@ -292,7 +292,7 @@ Example response: "iid" : 1, "assignee" : { "avatar_url" : null, - "web_url" : "https://gitlab.example.com/u/lennie", + "web_url" : "https://gitlab.example.com/lennie", "state" : "active", "username" : "lennie", "id" : 9, @@ -357,7 +357,7 @@ Example response: "name" : "Alexandra Bashirian", "avatar_url" : null, "state" : "active", - "web_url" : "https://gitlab.example.com/u/eileen.lowe", + "web_url" : "https://gitlab.example.com/eileen.lowe", "id" : 18, "username" : "eileen.lowe" }, @@ -414,7 +414,7 @@ Example response: "username" : "eileen.lowe", "id" : 18, "state" : "active", - "web_url" : "https://gitlab.example.com/u/eileen.lowe" + "web_url" : "https://gitlab.example.com/eileen.lowe" }, "state" : "closed", "title" : "Issues with auth", @@ -500,7 +500,7 @@ Example response: "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/axel.block" + "web_url": "https://gitlab.example.com/axel.block" }, "author": { "name": "Kris Steuber", @@ -508,7 +508,7 @@ Example response: "id": 10, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/solon.cremin" + "web_url": "https://gitlab.example.com/solon.cremin" }, "due_date": null, "web_url": "http://example.com/example/example/issues/11", @@ -557,7 +557,7 @@ Example response: "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/axel.block" + "web_url": "https://gitlab.example.com/axel.block" }, "author": { "name": "Kris Steuber", @@ -565,7 +565,7 @@ Example response: "id": 10, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/solon.cremin" + "web_url": "https://gitlab.example.com/solon.cremin" }, "due_date": null, "web_url": "http://example.com/example/example/issues/11", @@ -614,7 +614,7 @@ Example response: "id": 21, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/keyon" + "web_url": "https://gitlab.example.com/keyon" }, "author": { "name": "Vivian Hermann", @@ -622,7 +622,7 @@ Example response: "id": 11, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/orville" + "web_url": "https://gitlab.example.com/orville" }, "subscribed": false, "due_date": null, @@ -669,7 +669,7 @@ Example response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/root" + "web_url": "https://gitlab.example.com/root" }, "action_name": "marked", "target_type": "Issue", @@ -700,7 +700,7 @@ Example response: "id": 14, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/francisca" + "web_url": "https://gitlab.example.com/francisca" }, "author": { "name": "Maxie Medhurst", @@ -708,7 +708,7 @@ Example response: "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/craig_rutherford" + "web_url": "https://gitlab.example.com/craig_rutherford" }, "subscribed": true, "user_notes_count": 7, diff --git a/doc/api/keys.md b/doc/api/keys.md index faa6f212b433a2ae9e1bb1b3984ff93d14ad2bef..b68f08a007d53dc81ffc82aca1de8aecddd46bbf 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -24,7 +24,7 @@ Parameters: "id": 25, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon", - "web_url": "http://localhost:3000/u/john_smith", + "web_url": "http://localhost:3000/john_smith", "created_at": "2015-09-03T07:24:01.670Z", "is_admin": false, "bio": null, diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 494040a1ce83e75c5a0f3eb24c31e93103157e3d..f4167403c2cd0d2ce11a69992a999b915d28213b 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -621,7 +621,7 @@ Example response when the GitLab issue tracker is used: "author" : { "state" : "active", "id" : 18, - "web_url" : "https://gitlab.example.com/u/eileen.lowe", + "web_url" : "https://gitlab.example.com/eileen.lowe", "name" : "Alexandra Bashirian", "avatar_url" : null, "username" : "eileen.lowe" @@ -642,7 +642,7 @@ Example response when the GitLab issue tracker is used: "state" : "active", "id" : 1, "name" : "Administrator", - "web_url" : "https://gitlab.example.com/u/root", + "web_url" : "https://gitlab.example.com/root", "avatar_url" : null, "username" : "root" }, @@ -711,7 +711,7 @@ Example response: "id": 19, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/leila" + "web_url": "https://gitlab.example.com/leila" }, "assignee": { "name": "Celine Wehner", @@ -719,7 +719,7 @@ Example response: "id": 16, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/carli" + "web_url": "https://gitlab.example.com/carli" }, "source_project_id": 5, "target_project_id": 5, @@ -787,7 +787,7 @@ Example response: "id": 19, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/leila" + "web_url": "https://gitlab.example.com/leila" }, "assignee": { "name": "Celine Wehner", @@ -795,7 +795,7 @@ Example response: "id": 16, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/carli" + "web_url": "https://gitlab.example.com/carli" }, "source_project_id": 5, "target_project_id": 5, @@ -858,7 +858,7 @@ Example response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/root" + "web_url": "https://gitlab.example.com/root" }, "action_name": "marked", "target_type": "MergeRequest", @@ -881,7 +881,7 @@ Example response: "id": 14, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/francisca" + "web_url": "https://gitlab.example.com/francisca" }, "assignee": { "name": "Dr. Gabrielle Strosin", @@ -889,7 +889,7 @@ Example response: "id": 4, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/barrett.krajcik" + "web_url": "https://gitlab.example.com/barrett.krajcik" }, "source_project_id": 3, "target_project_id": 3, diff --git a/doc/api/notes.md b/doc/api/notes.md index 572844b8b3f7c22323c05b447afc34d4bb93f01c..58d40eecf3e3b33390d89e531713f6a3c8e16a05 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -143,7 +143,7 @@ Example Response: "state": "active", "created_at": "2013-09-30T13:46:01Z", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/pipin" + "web_url": "https://gitlab.example.com/pipin" }, "created_at": "2016-04-05T22:10:44.164Z", "system": false, @@ -268,7 +268,7 @@ Example Response: "state": "active", "created_at": "2013-09-30T13:46:01Z", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/pipin" + "web_url": "https://gitlab.example.com/pipin" }, "created_at": "2016-04-06T16:51:53.239Z", "system": false, @@ -398,7 +398,7 @@ Example Response: "state": "active", "created_at": "2013-09-30T13:46:01Z", "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/pipin" + "web_url": "https://gitlab.example.com/pipin" }, "created_at": "2016-04-05T22:11:59.923Z", "system": false, diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 847408a7f617dec115b5f08de5534f6b43427032..a29b3eb6f44527a8de0e206fb5c276cbab5dc917 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -34,7 +34,7 @@ Example of response "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "created_at": "2016-08-16T10:23:19.007Z", "updated_at": "2016-08-16T10:23:19.216Z", @@ -57,7 +57,7 @@ Example of response "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "created_at": "2016-08-16T10:23:21.184Z", "updated_at": "2016-08-16T10:23:21.314Z", @@ -103,7 +103,7 @@ Example of response "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", @@ -148,7 +148,7 @@ Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", @@ -193,7 +193,7 @@ Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "created_at": "2016-08-11T11:28:34.085Z", "updated_at": "2016-08-11T11:32:35.169Z", diff --git a/doc/api/projects.md b/doc/api/projects.md index f96bf7f6d63f0277b176b4e43caa229fdeb2f688..b7791b4748a963282e5cab88e9d9e869178b3bbb 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -465,7 +465,7 @@ Parameters: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "root" }, @@ -482,7 +482,7 @@ Parameters: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "john", "data": { @@ -528,7 +528,7 @@ Parameters: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "root" }, @@ -552,7 +552,7 @@ Parameters: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "created_at": "2015-12-04T10:33:56.698Z", "system": false, @@ -567,7 +567,7 @@ Parameters: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "root" } diff --git a/doc/api/todos.md b/doc/api/todos.md index 0cd644dfd2fe2bbd4e9e84cab7b990fa2e1fecda..a5e818010247a750a9542f1cf5051477c741611d 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -44,7 +44,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/root" + "web_url": "https://gitlab.example.com/root" }, "action_name": "marked", "target_type": "MergeRequest", @@ -67,7 +67,7 @@ Example Response: "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/craig_rutherford" + "web_url": "https://gitlab.example.com/craig_rutherford" }, "assignee": { "name": "Administrator", @@ -75,7 +75,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/root" + "web_url": "https://gitlab.example.com/root" }, "source_project_id": 2, "target_project_id": 2, @@ -117,7 +117,7 @@ Example Response: "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/craig_rutherford" + "web_url": "https://gitlab.example.com/craig_rutherford" }, "action_name": "assigned", "target_type": "MergeRequest", @@ -140,7 +140,7 @@ Example Response: "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/craig_rutherford" + "web_url": "https://gitlab.example.com/craig_rutherford" }, "assignee": { "name": "Administrator", @@ -148,7 +148,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/root" + "web_url": "https://gitlab.example.com/root" }, "source_project_id": 2, "target_project_id": 2, @@ -215,7 +215,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/root" + "web_url": "https://gitlab.example.com/root" }, "action_name": "marked", "target_type": "MergeRequest", @@ -238,7 +238,7 @@ Example Response: "id": 12, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/craig_rutherford" + "web_url": "https://gitlab.example.com/craig_rutherford" }, "assignee": { "name": "Administrator", @@ -246,7 +246,7 @@ Example Response: "id": 1, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "https://gitlab.example.com/u/root" + "web_url": "https://gitlab.example.com/root" }, "source_project_id": 2, "target_project_id": 2, diff --git a/doc/api/users.md b/doc/api/users.md index a52b2d51d78118b07e2ef6daebc474ecacbf70a2..2b12770d5a58b2749e3867b7e73dfb24dfbb46d9 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -20,7 +20,7 @@ GET /users "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", - "web_url": "http://localhost:3000/u/john_smith" + "web_url": "http://localhost:3000/john_smith" }, { "id": 2, @@ -28,7 +28,7 @@ GET /users "name": "Jack Smith", "state": "blocked", "avatar_url": "http://gravatar.com/../e32131cd8.jpeg", - "web_url": "http://localhost:3000/u/jack_smith" + "web_url": "http://localhost:3000/jack_smith" } ] ``` @@ -48,7 +48,7 @@ GET /users "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", - "web_url": "http://localhost:3000/u/john_smith", + "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", "is_admin": false, "bio": null, @@ -81,7 +81,7 @@ GET /users "name": "Jack Smith", "state": "blocked", "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", - "web_url": "http://localhost:3000/u/jack_smith", + "web_url": "http://localhost:3000/jack_smith", "created_at": "2012-05-23T08:01:01Z", "is_admin": false, "bio": null, @@ -141,7 +141,7 @@ Parameters: "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", - "web_url": "http://localhost:3000/u/john_smith", + "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", "is_admin": false, "bio": null, @@ -172,7 +172,7 @@ Parameters: "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", - "web_url": "http://localhost:3000/u/john_smith", + "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", "is_admin": false, "bio": null, @@ -293,7 +293,7 @@ GET /user "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", - "web_url": "http://localhost:3000/u/john_smith", + "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", "is_admin": false, "bio": null, @@ -665,7 +665,7 @@ Example response: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "root" }, @@ -682,7 +682,7 @@ Example response: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "john", "data": { @@ -728,7 +728,7 @@ Example response: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "root" }, @@ -752,7 +752,7 @@ Example response: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "created_at": "2015-12-04T10:33:56.698Z", "system": false, @@ -767,7 +767,7 @@ Example response: "id": 1, "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png", - "web_url": "http://localhost:3000/u/root" + "web_url": "http://localhost:3000/root" }, "author_username": "root" } diff --git a/doc/development/performance.md b/doc/development/performance.md index 65d348290250cc1157312c7d5f63a249471a6f6c..c4a964d1da3d84a4a7a91e7457b9d71fe7856fa7 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -254,5 +254,5 @@ impact on runtime performance, and as such, using a constant instead of referencing an object directly may even slow code down. [#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607 -[yorickpeterse]: https://gitlab.com/u/yorickpeterse +[yorickpeterse]: https://gitlab.com/yorickpeterse [anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern diff --git a/doc/raketasks/backup_hrz.png b/doc/raketasks/backup_hrz.png index 42084717ebe249e49fd9ac6574ef65f2868bb2ab..287587609a1aee884767e62dd6066847a60f443b 100644 Binary files a/doc/raketasks/backup_hrz.png and b/doc/raketasks/backup_hrz.png differ diff --git a/features/explore/projects.feature b/features/explore/projects.feature index 092e18d1b8697f0ad720aa9626e1ef14541c4366..4e0f4486ab7ac26c302e3c6bfdc03202374c792a 100644 --- a/features/explore/projects.feature +++ b/features/explore/projects.feature @@ -128,6 +128,7 @@ Feature: Explore Projects And project "Archive" has comments And I sign in as a user And project "Community" has comments + And trending projects are refreshed When I visit the explore trending projects Then I should see project "Community" And I should not see project "Internal" diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index b08912de25f014cddb06036ce55bddf78ec5cf5f..244306e8464719332d73e833caa35c6584fa74c0 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -21,7 +21,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(response_headers['Content-Type']).to have_content("application/atom+xml") expect(body).to have_selector("title", text: "#{@project.name}:master commits") expect(body).to have_selector("author email", text: commit.author_email) - expect(body).to have_selector("entry summary", text: commit.description[0..10]) + expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r")) end step 'I click on tag link' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index de065dffbc24abca2b9596fe80ca6e3dc00d1981..44346d99f4492401f56a0a34d04834f1443e4a5a 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -512,6 +512,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see new target branch changes' do expect(page).to have_content 'Request to merge fix into feature' expect(page).to have_content 'Target branch changed from merge-test to feature' + wait_for_ajax end step 'I click on "Email Patches"' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index afbd8ef123306da2ff89e4402e98c7ff98dd8cb9..cab85a48396b69465c2e0b2013ff532836529360 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -218,6 +218,10 @@ module SharedProject 2.times { create(:note_on_issue, project: project) } end + step 'trending projects are refreshed' do + TrendingProject.refresh! + end + step 'project "Shop" has labels: "bug", "feature", "enhancement"' do project = Project.find_by(name: "Shop") create(:label, project: project, title: 'bug') diff --git a/lib/banzai/filter/set_direction_filter.rb b/lib/banzai/filter/set_direction_filter.rb new file mode 100644 index 0000000000000000000000000000000000000000..c2976aeb7c6dff663fa95e389722b2ed2a7cdc4f --- /dev/null +++ b/lib/banzai/filter/set_direction_filter.rb @@ -0,0 +1,15 @@ +module Banzai + module Filter + # HTML filter that sets dir="auto" for RTL languages support + class SetDirectionFilter < HTML::Pipeline::Filter + def call + # select these elements just on top level of the document + doc.xpath('p|h1|h2|h3|h4|h5|h6|ol|ul[not(@class="section-nav")]|blockquote|table').each do |el| + el['dir'] = 'auto' + end + + doc + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 8d94b199c6680194be3b8ff2b53e37f63de0d865..5da2d0b008c860ad9c27c49506aabcc05ce2c6b4 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -25,7 +25,9 @@ module Banzai Filter::MilestoneReferenceFilter, Filter::TaskListFilter, - Filter::InlineDiffFilter + Filter::InlineDiffFilter, + + Filter::SetDirectionFilter ] end diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index dff9e29c6a5f8fdf0562c6ab0616405e802de26d..c843315782dc1eb167c4dfecda4f99024c75af1f 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -4,7 +4,7 @@ module Gitlab include Gitlab::Routing.url_helpers include IconsHelper - class MissingResolution < StandardError + class MissingResolution < ResolutionError end CONTEXT_LINES = 3 @@ -21,12 +21,34 @@ module Gitlab @match_line_headers = {} end + def content + merge_file_result[:data] + end + + def our_blob + @our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path) + end + + def type + lines unless @type + + @type.inquiry + end + # Array of Gitlab::Diff::Line objects def lines - @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data], + 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 end def resolve_lines(resolution) @@ -53,6 +75,14 @@ module Gitlab end.compact end + def resolve_content(resolution) + if resolution == content + raise MissingResolution, "Resolved content has no changes for file #{our_path}" + end + + resolution + end + def highlight_lines! their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n") our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n") @@ -170,21 +200,39 @@ module Gitlab match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}" end - def as_json(opts = nil) - { + def as_json(opts = {}) + json_hash = { old_path: their_path, new_path: our_path, blob_icon: file_type_icon_class('file', our_mode, our_path), blob_path: namespace_project_blob_path(merge_request.project.namespace, merge_request.project, - ::File.join(merge_request.diff_refs.head_sha, our_path)), - sections: sections + ::File.join(merge_request.diff_refs.head_sha, our_path)) } + + json_hash.tap do |json_hash| + if opts[:full_content] + json_hash[:content] = content + json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode) + else + json_hash[:sections] = sections if type.text? + json_hash[:type] = type + json_hash[:content_path] = content_path + end + end + end + + def content_path + conflict_for_path_namespace_project_merge_request_path(merge_request.project.namespace, + merge_request.project, + merge_request, + old_path: their_path, + 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].map do |instance_variable| + instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable| value = instance_variable_get("@#{instance_variable}") "#{instance_variable}=\"#{value}\"" diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index bbd0427a2c82766707c6079447915148c065930e..fa5bd4649d473c619d1a4d75baaf1b703892aaed 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -30,6 +30,10 @@ module Gitlab end end + def file_for_path(old_path, new_path) + files.find { |file| file.their_path == old_path && file.our_path == new_path } + end + def as_json(opts = nil) { target_branch: merge_request.target_branch, diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb index 98e842cded36d2db71b0ef8dc1ce1a78aab8a568..ddd657903fb6ab08625218bc37ece897ec98143c 100644 --- a/lib/gitlab/conflict/parser.rb +++ b/lib/gitlab/conflict/parser.rb @@ -1,19 +1,24 @@ module Gitlab module Conflict class Parser - class ParserError < StandardError + class UnresolvableError < StandardError end - class UnexpectedDelimiter < ParserError + class UnmergeableFile < UnresolvableError end - class MissingEndDelimiter < ParserError + class UnsupportedEncoding < UnresolvableError + end + + # Recoverable errors - the conflict can be resolved in an editor, but not with + # sections. + class ParserError < StandardError end - class UnmergeableFile < ParserError + class UnexpectedDelimiter < ParserError end - class UnsupportedEncoding < ParserError + class MissingEndDelimiter < ParserError end def parse(text, our_path:, their_path:, parent_file: nil) diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb new file mode 100644 index 0000000000000000000000000000000000000000..a0f2006bc245b7bc22811d11d85dda466a0fb9a6 --- /dev/null +++ b/lib/gitlab/conflict/resolution_error.rb @@ -0,0 +1,6 @@ +module Gitlab + module Conflict + class ResolutionError < StandardError + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index d509f0f2b9685078bcae3402e7eed73d7799c3f1..d6980471ea49d852a86a32e202dedb2979835cdd 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -570,7 +570,7 @@ describe Projects::MergeRequestsController 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::UnexpectedDelimiter) + to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) get :conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, @@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do format: 'json' end + it 'matches the schema' do + expect(response).to match_response_schema('conflicts') + end + it 'includes meta info about the MR' do expect(json_response['commit_message']).to include('Merge branch') expect(json_response['commit_sha']).to match(/\h{40}/) @@ -658,26 +662,97 @@ describe Projects::MergeRequestsController do end end + describe 'GET conflict_for_path' do + let(:json_response) { JSON.parse(response.body) } + + def conflict_for_path(path) + get :conflict_for_path, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project.to_param, + id: merge_request_with_conflicts.iid, + old_path: path, + new_path: path, + format: 'json' + end + + 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) + + conflict_for_path('files/ruby/regex.rb') + end + + it 'returns a 404 status code' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when the file does not exist cannot be resolved in the UI' do + before { conflict_for_path('files/ruby/regexp.rb') } + + it 'returns a 404 status code' do + expect(response).to have_http_status(:not_found) + end + end + + context 'with an existing file' do + let(:path) { 'files/ruby/regex.rb' } + + before { conflict_for_path(path) } + + it 'returns a 200 status code' do + expect(response).to have_http_status(:ok) + end + + it 'returns the file in JSON format' do + content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content + + expect(json_response).to include('old_path' => path, + 'new_path' => path, + 'blob_icon' => 'file-text-o', + 'blob_path' => a_string_ending_with(path), + 'blob_ace_mode' => 'ruby', + 'content' => content) + end + end + end + context 'POST resolve_conflicts' do let(:json_response) { JSON.parse(response.body) } let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } - def resolve_conflicts(sections) + def resolve_conflicts(files) post :resolve_conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, project_id: merge_request_with_conflicts.project.to_param, id: merge_request_with_conflicts.iid, format: 'json', - sections: sections, + files: files, commit_message: 'Commit message' end context 'with valid params' do before do - resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin') + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) end it 'creates a new commit on the branch' do @@ -692,7 +767,23 @@ describe Projects::MergeRequestsController do context 'when sections are missing' do before do - resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head') + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' + } + } + ] + + resolve_conflicts(resolved_files) end it 'returns a 400 error' do @@ -700,7 +791,71 @@ describe Projects::MergeRequestsController do end it 'has a message with the name of the first missing section' do - expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9') + expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + context 'when files are missing' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the name of the missing file' do + expect(json_response['message']).to include('files/ruby/popen.rb') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + context 'when a file has identical content to the conflict' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the path of the problem file' do + expect(json_response['message']).to include('files/ruby/popen.rb') end it 'does not create a new commit' do diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index a8833194421fca4eb355e158ad1c1a2cf8935aa9..f8c3ccb416b022a802d7438e728233f6877f96a1 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -53,7 +53,7 @@ describe "User Feed", feature: true do end it 'has XHTML summaries in issue descriptions' do - expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/ + expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p dir="auto">I guess/ end it 'has XHTML summaries in notes' do diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 759edf8ec80c5bb968604701df27ce9e3a987504..d258ff52bbb7eb326cc77cdb569302f96a4ff5aa 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -12,29 +12,139 @@ feature 'Merge request conflict resolution', js: true, feature: true do end end - context 'when a merge request can be resolved in the UI' do - let(:merge_request) { create_merge_request('conflict-resolvable') } + shared_examples "conflicts are resolved in Interactive mode" do + it 'conflicts are resolved in Interactive mode' do + within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do + click_button 'Use ours' + end + + within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do + all('button', text: 'Use ours').each do |button| + button.click + end + end + + click_button 'Commit conflict resolution' + wait_for_ajax + + expect(page).to have_content('All merge conflicts were resolved') + merge_request.reload_diff + + click_on 'Changes' + wait_for_ajax + + within find('.diff-file', text: 'files/ruby/popen.rb') do + expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }") + expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }") + end + + within find('.diff-file', text: 'files/ruby/regex.rb') do + expect(page).to have_selector('.line_content.new', text: "def username_regexp") + expect(page).to have_selector('.line_content.new', text: "def project_name_regexp") + expect(page).to have_selector('.line_content.new', text: "def path_regexp") + expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp") + expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp") + expect(page).to have_selector('.line_content.new', text: "def default_regexp") + end + end + end + shared_examples "conflicts are resolved in Edit inline mode" do + it 'conflicts are resolved in Edit inline mode' do + expect(find('#conflicts')).to have_content('popen.rb') + + within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do + click_button 'Edit inline' + wait_for_ajax + execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");') + end + + within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do + click_button 'Edit inline' + wait_for_ajax + execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') + end + + click_button 'Commit conflict resolution' + wait_for_ajax + expect(page).to have_content('All merge conflicts were resolved') + merge_request.reload_diff + + click_on 'Changes' + wait_for_ajax + + expect(page).to have_content('One morning') + expect(page).to have_content('Gregor Samsa woke from troubled dreams') + end + end + + context 'can be resolved in the UI' do before do project.team << [user, :developer] login_as(user) - - visit namespace_project_merge_request_path(project.namespace, project, merge_request) end - it 'shows a link to the conflict resolution page' do - expect(page).to have_link('conflicts', href: /\/conflicts\Z/) + context 'the conflicts are resolvable' do + let(:merge_request) { create_merge_request('conflict-resolvable') } + + before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) } + + it 'shows a link to the conflict resolution page' do + expect(page).to have_link('conflicts', href: /\/conflicts\Z/) + end + + context 'in Inline view mode' do + before { click_link('conflicts', href: /\/conflicts\Z/) } + + include_examples "conflicts are resolved in Interactive mode" + include_examples "conflicts are resolved in Edit inline mode" + end + + context 'in Parallel view mode' do + before do + click_link('conflicts', href: /\/conflicts\Z/) + click_button 'Side-by-side' + end + + include_examples "conflicts are resolved in Interactive mode" + include_examples "conflicts are resolved in Edit inline mode" + end end - context 'visiting the conflicts resolution page' do - before { click_link('conflicts', href: /\/conflicts\Z/) } + context 'the conflict contain markers' do + let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') } - it 'shows the conflicts' do - begin - expect(find('#conflicts')).to have_content('popen.rb') - rescue Capybara::Poltergeist::JavascriptError - retry + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + click_link('conflicts', href: /\/conflicts\Z/) + end + + it 'conflicts can not be resolved in Interactive mode' do + within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do + expect(page).not_to have_content 'Interactive mode' + expect(page).not_to have_content 'Edit inline' + end + end + + it 'conflicts are resolved in Edit inline mode' do + within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do + wait_for_ajax + execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");') end + + click_button 'Commit conflict resolution' + wait_for_ajax + + expect(page).to have_content('All merge conflicts were resolved') + + merge_request.reload_diff + + click_on 'Changes' + wait_for_ajax + find('.click-to-expand').click + wait_for_ajax + + expect(page).to have_content('Gregor Samsa woke from troubled dreams') end end end @@ -42,7 +152,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do UNRESOLVABLE_CONFLICTS = { 'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-binary-file' => 'when the conflicts contain a binary file', - 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers', 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another', 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file', } diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..fc88fd74af879e53c28b1c895c3d5305146d2239 --- /dev/null +++ b/spec/features/projects/files/find_file_keyboard_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +feature 'Find file keyboard shortcuts', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.team << [user, :master] + login_as user + + visit namespace_project_find_file_path(project.namespace, project, project.repository.root_ref) + + wait_for_ajax + end + + it 'opens file when pressing enter key' do + fill_in 'file_find', with: 'CHANGELOG' + + find('#file_find').native.send_keys(:enter) + + expect(page).to have_selector('.blob-content-holder') + + page.within('.file-title') do + expect(page).to have_content('CHANGELOG') + end + end + + it 'navigates files with arrow keys' do + fill_in 'file_find', with: 'application.' + + find('#file_find').native.send_keys(:down) + find('#file_find').native.send_keys(:enter) + + expect(page).to have_selector('.blob-content-holder') + + page.within('.file-title') do + expect(page).to have_content('application.js') + end + end +end diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index ec4c4d62f535cef814da21e1c19cc6c98928379f..111ca7f7a703a05963cb8f8e26e1aba47afab244 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -51,6 +51,18 @@ feature 'Users', feature: true, js: true do expect(current_path).to eq user_path(user) expect(page).to have_text(user.name) end + + scenario '/u/user1/groups redirects to user groups page' do + visit '/u/user1/groups' + + expect(current_path).to eq user_groups_path(user) + end + + scenario '/u/user1/projects redirects to user projects page' do + visit '/u/user1/projects' + + expect(current_path).to eq user_projects_path(user) + end end feature 'username validation' do diff --git a/spec/fixtures/api/schemas/conflicts.json b/spec/fixtures/api/schemas/conflicts.json new file mode 100644 index 0000000000000000000000000000000000000000..a947783d505c59690b3c085feedd349b9cfcf83e --- /dev/null +++ b/spec/fixtures/api/schemas/conflicts.json @@ -0,0 +1,137 @@ +{ + "type": "object", + "required": [ + "commit_message", + "commit_sha", + "source_branch", + "target_branch", + "files" + ], + "properties": { + "commit_message": {"type": "string"}, + "commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "source_branch": {"type": "string"}, + "target_branch": {"type": "string"}, + "files": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/conflict-text-with-sections" }, + { "$ref": "#/definitions/conflict-text-for-editor" } + ] + } + } + }, + "definitions": { + "conflict-base": { + "type": "object", + "required": [ + "old_path", + "new_path", + "blob_icon", + "blob_path" + ], + "properties": { + "old_path": {"type": "string"}, + "new_path": {"type": "string"}, + "blob_icon": {"type": "string"}, + "blob_path": {"type": "string"} + } + }, + "conflict-text-for-editor": { + "allOf": [ + {"$ref": "#/definitions/conflict-base"}, + { + "type": "object", + "required": [ + "type", + "content_path" + ], + "properties": { + "type": {"type": {"enum": ["text-editor"]}}, + "content_path": {"type": "string"} + } + } + ] + }, + "conflict-text-with-sections": { + "allOf": [ + {"$ref": "#/definitions/conflict-base"}, + { + "type": "object", + "required": [ + "type", + "content_path", + "sections" + ], + "properties": { + "type": {"type": {"enum": ["text"]}}, + "content_path": {"type": "string"}, + "sections": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/section-context" }, + { "$ref": "#/definitions/section-conflict" } + ] + } + } + } + } + ] + }, + "section-base": { + "type": "object", + "required": [ + "conflict", + "lines" + ], + "properties": { + "conflict": {"type": "boolean"}, + "lines": { + "type": "array", + "items": { + "type": "object", + "required": [ + "old_line", + "new_line", + "text", + "rich_text" + ], + "properties": { + "type": {"type": "string"}, + "old_line": {"type": "string"}, + "new_line": {"type": "string"}, + "text": {"type": "string"}, + "rich_text": {"type": "string"} + } + } + } + } + }, + "section-context": { + "allOf": [ + {"$ref": "#/definitions/section-base"}, + { + "type": "object", + "properties": { + "conflict": {"enum": [false]} + } + } + ] + }, + "section-conflict": { + "allOf": [ + {"$ref": "#/definitions/section-base"}, + { + "type": "object", + "required": ["id"], + "properties": { + "conflict": {"enum": [true]}, + "id": {"type": "string"} + } + } + ] + } + } +} diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 90da78a67dd666d8d08832583e805c5ead564614..6bcda87c99971cd00bdce9c0a86b4da8f0f7bb6d 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -24,7 +24,7 @@ describe Banzai::ObjectRenderer do with(an_instance_of(Array)). and_call_original - expect(object).to receive(:redacted_note_html=).with('<p>hello</p>') + expect(object).to receive(:redacted_note_html=).with('<p dir="auto">hello</p>') expect(object).to receive(:user_visible_reference_count=).with(0) renderer.render([object], :note) @@ -92,10 +92,10 @@ describe Banzai::ObjectRenderer do docs = renderer.render_attributes(objects, :note) expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) - expect(docs[0].to_html).to eq('<p>hello</p>') + expect(docs[0].to_html).to eq('<p dir="auto">hello</p>') expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment) - expect(docs[1].to_html).to eq('<p>bye</p>') + expect(docs[1].to_html).to eq('<p dir="auto">bye</p>') end it 'returns when no objects to render' do diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb index 76f4207181065fb78202c6845c3f88d580ffec93..8cce1b96698da7b3b81358fb8fe45a274ddc7169 100644 --- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb @@ -4,11 +4,11 @@ describe Banzai::Pipeline::DescriptionPipeline do def parse(html) # When we pass HTML to Redcarpet, it gets wrapped in `p` tags... # ...except when we pass it pre-wrapped text. Rabble rabble. - unwrap = !html.start_with?('<p>') + unwrap = !html.start_with?('<p ') output = described_class.to_html(html, project: spy) - output.gsub!(%r{\A<p>(.*)</p>(.*)\z}, '\1\2') if unwrap + output.gsub!(%r{\A<p dir="auto">(.*)</p>(.*)\z}, '\1\2') if unwrap output end @@ -27,11 +27,17 @@ describe Banzai::Pipeline::DescriptionPipeline do end end - %w(b i strong em a ins del sup sub p).each do |elem| + %w(b i strong em a ins del sup sub).each do |elem| it "still allows '#{elem}' elements" do exp = act = "<#{elem}>Description</#{elem}>" expect(parse(act).strip).to eq exp end end + + it "still allows 'p' elements" do + exp = act = "<p dir=\"auto\">Description</p>" + + expect(parse(act).strip).to eq exp + end end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 60020487061b849dcda0dd40dea6ca0c783eea37..648d342ecf8e7d20f3c6e329c465f61968e37293 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -257,5 +257,16 @@ FILE it 'includes the blob icon for the file' do expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o') end + + context 'with the full_content option passed' do + it 'includes the full content of the conflict' do + expect(conflict_file.as_json(full_content: true)).to have_key(:content) + end + + it 'includes the detected language of the conflict file' do + expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]). + to eq('ruby') + end + end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 85820a9dc5729433c7d51ac3170c8f7487025797..caf191e11ae3dcfe0f60a693da83a9f000e23076 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -187,33 +187,24 @@ describe Ci::Pipeline, models: true do end end - describe "merge request metrics" do + describe 'merge request metrics' do let(:project) { FactoryGirl.create :project } let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } - context 'when transitioning to running' do - it 'records the build start time' do - time = Time.now - Timecop.freeze(time) { build.run } - - expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(time) - end - - it 'clears the build end time' do - build.run + before do + expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id) + end - expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil + context 'when transitioning to running' do + it 'schedules metrics workers' do + pipeline.run end end context 'when transitioning to success' do - it 'records the build end time' do - build.run - time = Time.now - Timecop.freeze(time) { build.success } - - expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(time) + it 'schedules metrics workers' do + pipeline.succeed end end end diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 15cd3a7ed7067184840ec27bbabd207dcf90001a..2e3702f7520c401a9094bba865db4cc481efef10 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -64,7 +64,7 @@ describe CacheMarkdownField do let(:html) { "<p><code>Foo</code></p>" } let(:updated_markdown) { "`Bar`" } - let(:updated_html) { "<p><code>Bar</code></p>" } + let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" } subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2167a988f998629b29f0833edbe01d4bde2ac134..1acc8d748afb5ab481bac85ea9e2f90d6cc30460 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1155,12 +1155,6 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey end - it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do - merge_request = create_merge_request('conflict-contains-conflict-markers') - - expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey - end - it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do merge_request = create_merge_request('conflict-missing-side') @@ -1172,6 +1166,12 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy end + + it 'returns a truthy value when the conflicts have to be resolved in an editor' do + merge_request = create_merge_request('conflict-contains-conflict-markers') + + expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy + end end describe "#forked_source_project_missing?" do diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 488dc1a63b0d0a1eb6bd5d0d3d5cbb91c7ebccfe..c18a2d55e438607d69acfab7cf83450a96a50527 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -15,27 +15,27 @@ describe UsersController, "routing" do end it "to #groups" do - expect(get("/u/User/groups")).to route_to('users#groups', username: 'User') + expect(get("/users/User/groups")).to route_to('users#groups', username: 'User') end it "to #projects" do - expect(get("/u/User/projects")).to route_to('users#projects', username: 'User') + expect(get("/users/User/projects")).to route_to('users#projects', username: 'User') end it "to #contributed" do - expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User') + expect(get("/users/User/contributed")).to route_to('users#contributed', username: 'User') end it "to #snippets" do - expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User') + expect(get("/users/User/snippets")).to route_to('users#snippets', username: 'User') end it "to #calendar" do - expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User') + expect(get("/users/User/calendar")).to route_to('users#calendar', username: 'User') end it "to #calendar_activities" do - expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') + expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') end end diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index d71932458fa049a80140f6337734b7fd6fe16c8d..388abb6a0dfc3045f0b82d7bdc0450a9f89fb54a 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do end describe '#execute' do - context 'with valid params' do + context 'with section params' do let(:params) do { - sections: { - '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - }, + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + sections: { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + sections: { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ], commit_message: 'This is a commit message!' } end @@ -49,7 +60,7 @@ describe MergeRequests::ResolveService do it 'creates a commit with the correct parents' do expect(merge_request.source_branch_head.parents.map(&:id)). to eq(['1450cd639e0bc6721eb02800169e464f212cde06', - '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + '824be604a34828eb682305f0d963056cfac87b2d']) end end @@ -74,8 +85,96 @@ describe MergeRequests::ResolveService do end end - context 'when a resolution is missing' do - let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } + context 'with content and sections params' do + let(:popen_content) { "class Popen\nend" } + + let(:params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: popen_content + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + sections: { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ], + commit_message: 'This is a commit message!' + } + end + + before do + MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + end + + it 'creates a commit with the message' do + expect(merge_request.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request.source_branch_head.parents.map(&:id)). + to eq(['1450cd639e0bc6721eb02800169e464f212cde06', + '824be604a34828eb682305f0d963056cfac87b2d']) + end + + it 'sets the content to the content given' do + blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha, + 'files/ruby/popen.rb') + + expect(blob.data).to eq(popen_content) + end + end + + context 'when a resolution section is missing' do + let(:invalid_params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: '' + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + sections: { '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' } + } + ], + commit_message: 'This is a commit message!' + } + end + + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingResolution error' do + expect { service.execute(merge_request) }. + to raise_error(Gitlab::Conflict::File::MissingResolution) + end + end + + context 'when the content of a file is unchanged' do + let(:invalid_params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: '' + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content + } + ], + commit_message: 'This is a commit message!' + } + end + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } it 'raises a MissingResolution error' do @@ -83,5 +182,27 @@ describe MergeRequests::ResolveService do to raise_error(Gitlab::Conflict::File::MissingResolution) end end + + context 'when a file is missing' do + let(:invalid_params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: '' + } + ], + commit_message: 'This is a commit message!' + } + end + + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingFiles error' do + expect { service.execute(merge_request) }. + to raise_error(MergeRequests::ResolveService::MissingFiles) + end + end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index ad8ae763f6dce8300b1872e5f9d2c30c6e96f064..c79975d8667ef71be6aafc13edd5b60521ebf36c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -27,10 +27,10 @@ module TestEnv 'expand-collapse-lines' => '238e82d', 'video' => '8879059', 'crlf-diff' => '5938907', - 'conflict-start' => '75284c7', + 'conflict-start' => '824be60', 'conflict-resolvable' => '1450cd6', 'conflict-binary-file' => '259a6fb', - 'conflict-contains-conflict-markers' => '5e0964c', + 'conflict-contains-conflict-markers' => '78a3086', 'conflict-missing-side' => 'eb227b3', 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', @@ -98,7 +98,9 @@ module TestEnv def setup_gitlab_shell unless File.directory?(Gitlab.config.gitlab_shell.path) - `rake gitlab:shell:install` + unless system('rake', 'gitlab:shell:install') + raise 'Can`t clone gitlab-shell' + end end end diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..232478c973526287049eee8ae7a4991dac3a07a4 --- /dev/null +++ b/spec/workers/pipeline_metrics_worker_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe PipelineMetricsWorker do + let(:project) { create(:project) } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } + + let(:pipeline) do + create(:ci_empty_pipeline, + status: status, + project: project, + ref: 'master', + sha: project.repository.commit('master').id, + started_at: 1.hour.ago, + finished_at: Time.now) + end + + describe '#perform' do + subject { described_class.new.perform(pipeline.id) } + + context 'when pipeline is running' do + let(:status) { 'running' } + + it 'records the build start time' do + subject + + expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(pipeline.started_at) + end + + it 'clears the build end time' do + subject + + expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil + end + end + + context 'when pipeline succeeded' do + let(:status) { 'success' } + + it 'records the build end time' do + subject + + expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(pipeline.finished_at) + end + end + end +end