diff options
56 files changed, 1032 insertions, 355 deletions
@@ -69,6 +69,10 @@ gem 'net-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order +# Before updating this gem, check if +# https://github.com/gollum/gollum-lib/pull/292 has been merged. +# If it has, then remove the monkey patch for update_page, rename_page and raw_data_in_committer +# in config/initializers/gollum.rb gem 'gollum-lib', '~> 4.2', require: false # Before updating this gem, check if diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index b65521b278f..64b7dd540f9 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -1,3 +1,7 @@ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; + export default class ProjectLabelSubscription { constructor(container) { this.$container = $(container); @@ -17,10 +21,7 @@ export default class ProjectLabelSubscription { $btn.addClass('disabled'); $span.toggleClass('hidden'); - $.ajax({ - type: 'POST', - url, - }).done(() => { + axios.post(url).then(() => { let newStatus; let newAction; @@ -45,6 +46,6 @@ export default class ProjectLabelSubscription { return button; }); - }); + }).catch(() => flash(__('There was an error subscribing to this label.'))); } } diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 55c93923cc8..59ad5b45855 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -1,3 +1,4 @@ +import axios from '../lib/utils/axios_utils'; import PANEL_STATE from './constants'; import { backOff } from '../lib/utils/common_utils'; @@ -81,24 +82,20 @@ export default class PrometheusMetrics { loadActiveMetrics() { this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); backOff((next, stop) => { - $.ajax({ - url: this.activeMetricsEndpoint, - dataType: 'json', - global: false, - }) - .done((res) => { - if (res && res.success) { - stop(res); + axios.get(this.activeMetricsEndpoint) + .then(({ data }) => { + if (data && data.success) { + stop(data); } else { this.backOffRequestCounter = this.backOffRequestCounter += 1; if (this.backOffRequestCounter < 3) { next(); } else { - stop(res); + stop(data); } } }) - .fail(stop); + .catch(stop); }) .then((res) => { if (res && res.data && res.data.length) { diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 632625da8e7..b51b3e9a6ff 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,5 +1,5 @@ -/* eslint-disable no-new */ -import Flash from '../flash'; +import flash from '../flash'; +import axios from '../lib/utils/axios_utils'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; export default class ProtectedBranchEdit { @@ -38,29 +38,25 @@ export default class ProtectedBranchEdit { this.$allowedToMergeDropdown.disable(); this.$allowedToPushDropdown.disable(); - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('access-level-id'), - access_level: $allowedToMergeInput.val(), - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('access-level-id'), - access_level: $allowedToPushInput.val(), - }], - }, + axios.patch(this.$wrap.data('url'), { + protected_branch: { + merge_access_levels_attributes: [{ + id: this.$allowedToMergeDropdown.data('access-level-id'), + access_level: $allowedToMergeInput.val(), + }], + push_access_levels_attributes: [{ + id: this.$allowedToPushDropdown.data('access-level-id'), + access_level: $allowedToPushInput.val(), + }], }, - error() { - new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); - }, - }).always(() => { + }).then(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }).catch(() => { this.$allowedToMergeDropdown.enable(); this.$allowedToPushDropdown.enable(); + + flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); }); } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index dad0ad25b65..21a258cf93c 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -1,5 +1,5 @@ -/* eslint-disable no-new */ -import Flash from '../flash'; +import flash from '../flash'; +import axios from '../lib/utils/axios_utils'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; export default class ProtectedTagEdit { @@ -28,24 +28,19 @@ export default class ProtectedTagEdit { this.$allowedToCreateDropdownButton.disable(); - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_tag: { - create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdownButton.data('access-level-id'), - access_level: $allowedToCreateInput.val(), - }], - }, + axios.patch(this.$wrap.data('url'), { + protected_tag: { + create_access_levels_attributes: [{ + id: this.$allowedToCreateDropdownButton.data('access-level-id'), + access_level: $allowedToCreateInput.val(), + }], }, - error() { - new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); - }, - }).always(() => { + }).then(() => { + this.$allowedToCreateDropdownButton.enable(); + }).catch(() => { this.$allowedToCreateDropdownButton.enable(); + + flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); }); } } diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b830fcf7e80..01c3be5411f 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -2,6 +2,8 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; +import flash from './flash'; +import axios from './lib/utils/axios_utils'; function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); @@ -62,7 +64,7 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { Sidebar.prototype.toggleTodo = function(e) { var $btnText, $this, $todoLoading, ajaxType, url; $this = $(e.currentTarget); - ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; + ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post'; if ($this.attr('data-delete-path')) { url = "" + ($this.attr('data-delete-path')); } else { @@ -71,25 +73,14 @@ Sidebar.prototype.toggleTodo = function(e) { $this.tooltip('hide'); - return $.ajax({ - url: url, - type: ajaxType, - dataType: 'json', - data: { - issuable_id: $this.data('issuable-id'), - issuable_type: $this.data('issuable-type') - }, - beforeSend: (function(_this) { - return function() { - $('.js-issuable-todo').disable() - .addClass('is-loading'); - }; - })(this) - }).done((function(_this) { - return function(data) { - return _this.todoUpdateDone(data); - }; - })(this)); + $('.js-issuable-todo').disable().addClass('is-loading'); + + axios[ajaxType](url, { + issuable_id: $this.data('issuable-id'), + issuable_type: $this.data('issuable-type'), + }).then(({ data }) => { + this.todoUpdateDone(data); + }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`)); }; Sidebar.prototype.todoUpdateDone = function(data) { diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index cd5ab53eace..c5dddd001bb 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,5 +1,6 @@ import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; +import axios from './lib/utils/axios_utils'; import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility'; import findAndFollowLink from './shortcuts_dashboard_navigation'; @@ -85,21 +86,21 @@ export default class Shortcuts { $modal.modal('toggle'); } - $.ajax({ - url: gon.shortcuts_path, - dataType: 'script', - success() { - if (location && location.length > 0) { - const results = []; - for (let i = 0, len = location.length; i < len; i += 1) { - results.push($(location[i]).show()); - } - return results; + return axios.get(gon.shortcuts_path, { + responseType: 'text', + }).then(({ data }) => { + $.globalEval(data); + + if (location && location.length > 0) { + const results = []; + for (let i = 0, len = location.length; i < len; i += 1) { + results.push($(location[i]).show()); } + return results; + } - $('.hidden-shortcut').show(); - return $('.js-more-help-button').remove(); - }, + $('.hidden-shortcut').show(); + return $('.js-more-help-button').remove(); }); } diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index dcbec40c79e..129a551cbcd 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,4 +1,5 @@ import 'deckar01-task_list'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; export default class TaskList { @@ -7,11 +8,11 @@ export default class TaskList { this.dataType = options.dataType; this.fieldName = options.fieldName; this.onSuccess = options.onSuccess || (() => {}); - this.onError = function showFlash(response) { + this.onError = function showFlash(e) { let errorMessages = ''; - if (response.responseJSON) { - errorMessages = response.responseJSON.errors.join(' '); + if (e.response.data && typeof e.response.data === 'object') { + errorMessages = e.response.data.errors.join(' '); } return new Flash(errorMessages || 'Update failed', 'alert'); @@ -38,12 +39,9 @@ export default class TaskList { patchData[this.dataType] = { [this.fieldName]: $target.val(), }; - return $.ajax({ - type: 'PATCH', - url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), - data: patchData, - success: this.onSuccess, - error: this.onError, - }); + + return axios.patch($target.data('update-url') || $('form.js-issuable-update').attr('action'), patchData) + .then(({ data }) => this.onSuccess(data)) + .catch(err => this.onError(err)); } } diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js index 992baa9a1ef..e13b9839a20 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/users/user_tabs.js @@ -1,6 +1,9 @@ +import axios from '../lib/utils/axios_utils'; import Activities from '../activities'; import ActivityCalendar from './activity_calendar'; import { localTimeAgo } from '../lib/utils/datetime_utility'; +import { __ } from '../locale'; +import flash from '../flash'; /** * UserTabs @@ -131,18 +134,20 @@ export default class UserTabs { } loadTab(action, endpoint) { - return $.ajax({ - beforeSend: () => this.toggleLoading(true), - complete: () => this.toggleLoading(false), - dataType: 'json', - url: endpoint, - success: (data) => { + this.toggleLoading(true); + + return axios.get(endpoint) + .then(({ data }) => { const tabSelector = `div#${action}`; this.$parentEl.find(tabSelector).html(data.html); this.loaded[action] = true; localTimeAgo($('.js-timeago', tabSelector)); - }, - }); + + this.toggleLoading(false); + }) + .catch(() => { + this.toggleLoading(false); + }); } loadActivities() { @@ -158,17 +163,15 @@ export default class UserTabs { utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`; } - $.ajax({ - dataType: 'json', - url: calendarPath, - success: (activityData) => { + axios.get(calendarPath) + .then(({ data }) => { $calendarWrap.html(CALENDAR_TEMPLATE); $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`); // eslint-disable-next-line no-new - new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath, utcOffset); - }, - }); + new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset); + }) + .catch(() => flash(__('There was an error loading users activity calendar.'))); // eslint-disable-next-line no-new new Activities(); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index ab108906732..eaed81cf79e 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -2,6 +2,7 @@ /* global Issuable */ /* global emitSidebarEvent */ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; @@ -177,32 +178,28 @@ function UsersSelect(currentUser, els, options = {}) { $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); - return $.ajax({ - type: 'PUT', - dataType: 'json', - url: issueURL, - data: data - }).done(function(data) { - var user; - $dropdown.trigger('loaded.gl.dropdown'); - $loading.fadeOut(); - if (data.assignee) { - user = { - name: data.assignee.name, - username: data.assignee.username, - avatar: data.assignee.avatar_url - }; - } else { - user = { - name: 'Unassigned', - username: '', - avatar: '' - }; - } - $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); - return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); - }); + return axios.put(issueURL, data) + .then(({ data }) => { + var user; + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + if (data.assignee) { + user = { + name: data.assignee.name, + username: data.assignee.username, + avatar: data.assignee.avatar_url + }; + } else { + user = { + name: 'Unassigned', + username: '', + avatar: '' + }; + } + $value.html(assigneeTemplate(user)); + $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); + return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); + }); }; collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); @@ -660,38 +657,33 @@ UsersSelect.prototype.user = function(user_id, callback) { var url; url = this.buildUrl(this.userPath); url = url.replace(':id', user_id); - return $.ajax({ - url: url, - dataType: "json" - }).done(function(user) { - return callback(user); - }); + return axios.get(url) + .then(({ data }) => { + callback(data); + }); }; // Return users list. Filtered by query // Only active users retrieved UsersSelect.prototype.users = function(query, options, callback) { - var url; - url = this.buildUrl(this.usersPath); - return $.ajax({ - url: url, - data: { - search: query, - per_page: options.perPage || 20, - active: true, - project_id: options.projectId || null, - group_id: options.groupId || null, - skip_ldap: options.skipLdap || null, - todo_filter: options.todoFilter || null, - todo_state_filter: options.todoStateFilter || null, - current_user: options.showCurrentUser || null, - author_id: options.authorId || null, - skip_users: options.skipUsers || null - }, - dataType: "json" - }).done(function(users) { - return callback(users); - }); + const url = this.buildUrl(this.usersPath); + const params = { + search: query, + per_page: options.perPage || 20, + active: true, + project_id: options.projectId || null, + group_id: options.groupId || null, + skip_ldap: options.skipLdap || null, + todo_filter: options.todoFilter || null, + todo_state_filter: options.todoStateFilter || null, + current_user: options.showCurrentUser || null, + author_id: options.authorId || null, + skip_users: options.skipUsers || null + }; + return axios.get(url, { params }) + .then(({ data }) => { + callback(data); + }); }; UsersSelect.prototype.buildUrl = function(url) { diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index d8fec583121..e70a57c2a67 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -6,6 +6,14 @@ } } +.wiki-form { + .edit-wiki-page-slug-tip { + display: inline-block; + max-width: 100%; + margin-top: 5px; + } +} + .title .edit-wiki-header { width: 780px; margin-left: auto; diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 61554029d09..7ad79a1e56c 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -70,7 +70,7 @@ module UploadsActions end def build_uploader_from_params - uploader = uploader_class.new(model, params[:secret]) + uploader = uploader_class.new(model, secret: params[:secret]) uploader.retrieve_from_store!(params[:filename]) uploader end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 292e4158f8b..fde96cbd35d 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -54,8 +54,8 @@ class Projects::WikisController < Projects::ApplicationController else render 'edit' end - rescue WikiPage::PageChangedError - @conflict = true + rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e + @error = e render 'edit' end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 815fab9e061..41f9eedd4bd 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -21,4 +21,22 @@ module WikiHelper add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after end end + + def wiki_page_errors(error) + return unless error + + content_tag(:div, class: 'alert alert-danger') do + case error + when WikiPage::PageChangedError + page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank" + concat( + (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe + ) + when WikiPage::PageRenameError + s_("WikiEdit|There is already a page with the same title in that path.") + else + error.message + end + end + end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 459d1673125..f6041da986c 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -119,6 +119,8 @@ class ProjectWiki end def delete_page(page, message = nil) + return unless page + wiki.delete_page(page.path, commit_details(:deleted, message, page.title)) update_project_activity @@ -131,6 +133,8 @@ class ProjectWiki end def page_title_and_dir(title) + return unless title + title_array = title.split("/") title = title_array.pop [title, title_array.join("/")] diff --git a/app/models/upload.rb b/app/models/upload.rb index fb55fd8007b..2024228537a 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -30,7 +30,7 @@ class Upload < ActiveRecord::Base end def build_uploader - uploader_class.new(model).tap do |uploader| + uploader_class.new(model, mount_point, **uploader_context).tap do |uploader| uploader.upload = self uploader.retrieve_from_store!(identifier) end @@ -40,6 +40,13 @@ class Upload < ActiveRecord::Base File.exist?(absolute_path) end + def uploader_context + { + identifier: identifier, + secret: secret + }.compact + end + private def checksummable? @@ -62,11 +69,15 @@ class Upload < ActiveRecord::Base !path.start_with?('/') end + def uploader_class + Object.const_get(uploader) + end + def identifier File.basename(path) end - def uploader_class - Object.const_get(uploader) + def mount_point + super&.to_sym end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index e6254183baf..0f5536415f7 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -1,5 +1,6 @@ class WikiPage PageChangedError = Class.new(StandardError) + PageRenameError = Class.new(StandardError) include ActiveModel::Validations include ActiveModel::Conversion @@ -102,7 +103,7 @@ class WikiPage # The hierarchy of the directory this page is contained in. def directory - wiki.page_title_and_dir(slug).last + wiki.page_title_and_dir(slug)&.last.to_s end # The processed/formatted content of this page. @@ -177,7 +178,7 @@ class WikiPage # Creates a new Wiki Page. # # attr - Hash of attributes to set on the new page. - # :title - The title for the new page. + # :title - The title (optionally including dir) for the new page. # :content - The raw markup content. # :format - Optional symbol representing the # content format. Can be any type @@ -189,7 +190,7 @@ class WikiPage # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. def create(attrs = {}) - @attributes.merge!(attrs) + update_attributes(attrs) save(page_details: title) do wiki.create_page(title, content, format, message) @@ -204,24 +205,29 @@ class WikiPage # See ProjectWiki::MARKUPS Hash for available formats. # :message - Optional commit message to set on the new version. # :last_commit_sha - Optional last commit sha to validate the page unchanged. - # :title - The Title to replace existing title + # :title - The Title (optionally including dir) to replace existing title # # Returns the String SHA1 of the newly created page # or False if the save was unsuccessful. def update(attrs = {}) last_commit_sha = attrs.delete(:last_commit_sha) + if last_commit_sha && last_commit_sha != self.last_commit_sha - raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.") + raise PageChangedError end - attrs.slice!(:content, :format, :message, :title) - @attributes.merge!(attrs) - page_details = - if title.present? && @page.title != title - title - else - @page.url_path + update_attributes(attrs) + + if title_changed? + page_details = title + + if wiki.find_page(page_details).present? + @attributes[:title] = @page.url_path + raise PageRenameError end + else + page_details = @page.url_path + end save(page_details: page_details) do wiki.update_page( @@ -255,8 +261,44 @@ class WikiPage page.version.to_s end + def title_changed? + title.present? && self.class.unhyphenize(@page.url_path) != title + end + private + # Process and format the title based on the user input. + def process_title(title) + return if title.blank? + + title = deep_title_squish(title) + current_dirname = File.dirname(title) + + if @page.present? + return title[1..-1] if current_dirname == '/' + return File.join([directory.presence, title].compact) if current_dirname == '.' + end + + title + end + + # This method squishes all the filename + # i.e: ' foo / bar / page_name' => 'foo/bar/page_name' + def deep_title_squish(title) + components = title.split(File::SEPARATOR).map(&:squish) + + File.join(components) + end + + # Updates the current @attributes hash by merging a hash of params + def update_attributes(attrs) + attrs[:title] = process_title(attrs[:title]) if attrs[:title].present? + + attrs.slice!(:content, :format, :message, :title) + + @attributes.merge!(attrs) + end + def set_attributes attributes[:slug] = @page.url_path attributes[:title] = @page.title diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb index e7af1483d23..8f56f09c9f7 100644 --- a/app/uploaders/file_mover.rb +++ b/app/uploaders/file_mover.rb @@ -49,11 +49,11 @@ class FileMover end def uploader - @uploader ||= PersonalFileUploader.new(model, secret) + @uploader ||= PersonalFileUploader.new(model, secret: secret) end def temp_file_uploader - @temp_file_uploader ||= PersonalFileUploader.new(nil, secret) + @temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret) end def revert diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 85ae9863b13..2310e67cb2e 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -62,9 +62,11 @@ class FileUploader < GitlabUploader attr_accessor :model - def initialize(model, secret = nil) + def initialize(model, mounted_as = nil, **uploader_context) + super(model, nil, **uploader_context) + @model = model - @secret = secret + apply_context!(uploader_context) end def base_dir @@ -107,15 +109,17 @@ class FileUploader < GitlabUploader self.file.filename end - # the upload does not hold the secret, but holds the path - # which contains the secret: extract it def upload=(value) + super + + return unless value + return if apply_context!(value.uploader_context) + + # fallback to the regex based extraction if matches = DYNAMIC_PATH_PATTERN.match(value.path) @secret = matches[:secret] @identifier = matches[:identifier] end - - super end def secret @@ -124,6 +128,18 @@ class FileUploader < GitlabUploader private + def apply_context!(uploader_context) + @secret, @identifier = uploader_context.values_at(:secret, :identifier) + + !!(@secret && @identifier) + end + + def build_upload + super.tap do |upload| + upload.secret = secret + end + end + def markdown_name (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]") end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index b12829efe73..a9e5c028b03 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -29,6 +29,10 @@ class GitlabUploader < CarrierWave::Uploader::Base delegate :base_dir, :file_storage?, to: :class + def initialize(model, mounted_as = nil, **uploader_context) + super(model, mounted_as) + end + def file_cache_storage? cache_storage.is_a?(CarrierWave::Storage::File) end diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index dfb8dccec57..458928bc067 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -24,7 +24,7 @@ module RecordsUploads uploads.where(path: upload_path).delete_all upload.destroy! if upload - self.upload = build_upload_from_uploader(self) + self.upload = build_upload upload.save! end end @@ -39,12 +39,13 @@ module RecordsUploads Upload.order(id: :desc).where(uploader: self.class.to_s) end - def build_upload_from_uploader(uploader) + def build_upload Upload.new( - size: uploader.file.size, - path: uploader.upload_path, - model: uploader.model, - uploader: uploader.class.to_s + uploader: self.class.to_s, + size: file.size, + path: upload_path, + model: model, + mount_point: mounted_as ) end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index d3e867e124c..888d820b04e 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -47,7 +47,7 @@ - if @repository.gitlab_ci_yml %li - = link_to _('CI configuration'), ci_configuration_path(@project) + = link_to _('CI/CD configuration'), ci_configuration_path(@project) - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog @@ -65,7 +65,7 @@ - unless @repository.gitlab_ci_yml %li.missing = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - #{ _('Set up CI') } + #{ _('Set up CI/CD') } - if koding_enabled? && @repository.koding_yml.blank? %li.missing = link_to _('Set up Koding'), add_koding_stack_path(@project) diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 4e265bf733a..d285251d06f 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -9,7 +9,13 @@ .form-group .col-sm-12= f.label :title, class: 'control-label-full-width' - .col-sm-12= f.text_field :title, class: 'form-control', value: @page.title + .col-sm-12 + = f.text_field :title, class: 'form-control', value: @page.title + - if @page.persisted? + %span.edit-wiki-page-slug-tip + = icon('lightbulb-o') + = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") + = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank' .form-group .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 0d77e5bd16d..9d3d4072027 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,10 +1,7 @@ - @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout - page_title _("Edit"), @page.title.capitalize, _("Wiki") -- if @conflict - .alert.alert-danger - - page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank" - = (s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe += wiki_page_errors(@error) .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } diff --git a/changelogs/unreleased/42547-upload-store-mount-point.yml b/changelogs/unreleased/42547-upload-store-mount-point.yml new file mode 100644 index 00000000000..35ae022984e --- /dev/null +++ b/changelogs/unreleased/42547-upload-store-mount-point.yml @@ -0,0 +1,5 @@ +--- +title: Added uploader metadata to the uploads. +merge_request: 16779 +author: +type: added diff --git a/changelogs/unreleased/42684-set-up-ci-set-up-ci-cd.yml b/changelogs/unreleased/42684-set-up-ci-set-up-ci-cd.yml new file mode 100644 index 00000000000..0ef28e2ee01 --- /dev/null +++ b/changelogs/unreleased/42684-set-up-ci-set-up-ci-cd.yml @@ -0,0 +1,5 @@ +--- +title: Rename button to enable CI/CD configuration to "Set up CI/CD" +merge_request: 16870 +author: +type: changed diff --git a/changelogs/unreleased/fj-37273-moving-wiki-pages-from-the-ui.yml b/changelogs/unreleased/fj-37273-moving-wiki-pages-from-the-ui.yml new file mode 100644 index 00000000000..5b5310dcfef --- /dev/null +++ b/changelogs/unreleased/fj-37273-moving-wiki-pages-from-the-ui.yml @@ -0,0 +1,5 @@ +--- +title: Allow moving wiki pages from the UI +merge_request: 16313 +author: +type: fixed diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb index 0b86cac51a7..6dfaceb8427 100644 --- a/config/initializers/gollum.rb +++ b/config/initializers/gollum.rb @@ -35,6 +35,88 @@ module Gollum [] end end + + # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged + def update_page(page, name, format, data, commit = {}) + name = name ? ::File.basename(name) : page.name + format ||= page.format + dir = ::File.dirname(page.path) + dir = '' if dir == '.' + filename = (rename = page.name != name) ? Gollum::Page.cname(name) : page.filename_stripped + + multi_commit = !!commit[:committer] + committer = multi_commit ? commit[:committer] : Committer.new(self, commit) + + if !rename && page.format == format + committer.add(page.path, normalize(data)) + else + committer.delete(page.path) + committer.add_to_index(dir, filename, format, data) + end + + committer.after_commit do |index, _sha| + @access.refresh + index.update_working_dir(dir, page.filename_stripped, page.format) + index.update_working_dir(dir, filename, format) + end + + multi_commit ? committer : committer.commit + end + + # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged + def rename_page(page, rename, commit = {}) + return false if page.nil? + return false if rename.nil? || rename.empty? + + (target_dir, target_name) = ::File.split(rename) + (source_dir, source_name) = ::File.split(page.path) + source_name = page.filename_stripped + + # File.split gives us relative paths with ".", commiter.add_to_index doesn't like that. + target_dir = '' if target_dir == '.' + source_dir = '' if source_dir == '.' + target_dir = target_dir.gsub(/^\//, '') # rubocop:disable Style/RegexpLiteral + + # if the rename is a NOOP, abort + if source_dir == target_dir && source_name == target_name + return false + end + + multi_commit = !!commit[:committer] + committer = multi_commit ? commit[:committer] : Committer.new(self, commit) + + # This piece only works for multi_commit + # If we are in a commit batch and one of the previous operations + # has updated the page, any information we ask to the page can be outdated. + # Therefore, we should ask first to the current committer tree to see if + # there is any updated change. + raw_data = raw_data_in_committer(committer, source_dir, page.filename) || + raw_data_in_committer(committer, source_dir, "#{target_name}.#{Page.format_to_ext(page.format)}") || + page.raw_data + + committer.delete(page.path) + committer.add_to_index(target_dir, target_name, page.format, raw_data) + + committer.after_commit do |index, _sha| + @access.refresh + index.update_working_dir(source_dir, source_name, page.format) + index.update_working_dir(target_dir, target_name, page.format) + end + + multi_commit ? committer : committer.commit + end + + # Remove if https://github.com/gollum/gollum-lib/pull/292 has been merged + def raw_data_in_committer(committer, dir, filename) + data = nil + + [*dir.split(::File::SEPARATOR), filename].each do |key| + data = data ? data[key] : committer.tree[key] + break unless data + end + + data + end end module Git diff --git a/db/migrate/20180129193323_add_uploads_builder_context.rb b/db/migrate/20180129193323_add_uploads_builder_context.rb new file mode 100644 index 00000000000..b3909a770ca --- /dev/null +++ b/db/migrate/20180129193323_add_uploads_builder_context.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddUploadsBuilderContext < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :uploads, :mount_point, :string + add_column :uploads, :secret, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index c56cb461320..432eb095746 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1751,6 +1751,8 @@ ActiveRecord::Schema.define(version: 20180204200836) do t.string "model_type" t.string "uploader", null: false t.datetime "created_at", null: false + t.string "mount_point" + t.string "secret" end add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 144cd4c26b0..0fab752afad 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -395,7 +395,7 @@ If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the Assuming that your project is new or it doesn't have a `.gitlab-ci.yml` file present: -1. From your project home page, either click on the "Set up CI" button, or click +1. From your project home page, either click on the "Set up CI/CD" button, or click on the plus button and (`+`), then "New file" 1. Pick `.gitlab-ci.yml` as the template type 1. Select "Auto-DevOps" from the template dropdown diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md index 64de0463dad..4a724dd5c1b 100644 --- a/doc/user/project/pages/getting_started_part_two.md +++ b/doc/user/project/pages/getting_started_part_two.md @@ -77,7 +77,7 @@ is useful for submitting merge requests to the upstream. > > 2. Why do I need to enable Shared Runners? > -> Shared Runners will run the script set by your GitLab CI +> Shared Runners will run the script set by your GitLab CI/CD configuration file. They're enabled by default to new projects, but not to forks. @@ -88,9 +88,9 @@ click **New project**, and name it considering the [practical examples](getting_started_part_one.md#practical-examples). 1. Clone it to your local computer, add your website files to your project, add, commit and push to GitLab. -1. From the your **Project**'s page, click **Set up CI**: +1. From the your **Project**'s page, click **Set up CI/CD**: - ![setup GitLab CI](img/setup_ci.png) + ![setup GitLab CI/CD](img/setup_ci.png) 1. Choose one of the templates from the dropbox menu. Pick up the template corresponding to the SSG you're using (or plain HTML). @@ -98,7 +98,7 @@ Pick up the template corresponding to the SSG you're using (or plain HTML). ![gitlab-ci templates](img/choose_ci_template.png) Once you have both site files and `.gitlab-ci.yml` in your project's -root, GitLab CI will build your site and deploy it with Pages. +root, GitLab CI/CD will build your site and deploy it with Pages. Once the first build passes, you see your site is live by navigating to your **Project**'s **Settings** > **Pages**, where you'll find its default URL. diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index db0c3ed9d59..33c9a1a4d6b 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -45,7 +45,7 @@ has already been created, which creates a link to the license itself. ![New file button](img/web_editor_template_dropdown_buttons.png) >**Note:** -The **Set up CI** button will not appear on an empty repository. You have to at +The **Set up CI/CD** button will not appear on an empty repository. You have to at least add a file in order for the button to show up. ## Upload a file diff --git a/doc/user/project/wiki/img/wiki_move_page_1.png b/doc/user/project/wiki/img/wiki_move_page_1.png Binary files differnew file mode 100644 index 00000000000..0331c9d3a5c --- /dev/null +++ b/doc/user/project/wiki/img/wiki_move_page_1.png diff --git a/doc/user/project/wiki/img/wiki_move_page_2.png b/doc/user/project/wiki/img/wiki_move_page_2.png Binary files differnew file mode 100644 index 00000000000..a8e0c055051 --- /dev/null +++ b/doc/user/project/wiki/img/wiki_move_page_2.png diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index c0b8a87f038..d084ee41d8a 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -64,6 +64,18 @@ effect. You can find the **Delete** button only when editing a page. Click on it and confirm you want the page to be deleted. +## Moving a wiki page + +You can move a wiki page from one directory to another by specifying the full +path in the wiki page title in the [edit](#editing-a-wiki-page) form. + +![Moving a page](img/wiki_move_page_1.png) + +![After moving a page](img/wiki_move_page_2.png) + +In order to move a wiki page to the root directory, the wiki page title must +be preceded by the slash (`/`) character. + ## Viewing a list of all created wiki pages Every wiki has a sidebar from which a short list of the created pages can be diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb index 3fdc3c27f73..1b74f735679 100644 --- a/lib/gitlab/gfm/uploads_rewriter.rb +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -46,7 +46,7 @@ module Gitlab private def find_file(project, secret, file) - uploader = FileUploader.new(project, secret) + uploader = FileUploader.new(project, secret: secret) uploader.retrieve_from_store!(file) uploader.file end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index ccdb8975342..daa17fc72cf 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -25,8 +25,9 @@ module Gitlab @repository.exists? end + # Disabled because of https://gitlab.com/gitlab-org/gitaly/merge_requests/539 def write_page(name, format, content, commit_details) - @repository.gitaly_migrate(:wiki_write_page) do |is_enabled| + @repository.gitaly_migrate(:wiki_write_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled| if is_enabled gitaly_write_page(name, format, content, commit_details) gollum_wiki.clear_cache @@ -47,8 +48,9 @@ module Gitlab end end + # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42094 def update_page(page_path, title, format, content, commit_details) - @repository.gitaly_migrate(:wiki_update_page) do |is_enabled| + @repository.gitaly_migrate(:wiki_update_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled| if is_enabled gitaly_update_page(page_path, title, format, content, commit_details) gollum_wiki.clear_cache @@ -68,8 +70,9 @@ module Gitlab end end + # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42039 def page(title:, version: nil, dir: nil) - @repository.gitaly_migrate(:wiki_find_page) do |is_enabled| + @repository.gitaly_migrate(:wiki_find_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled| if is_enabled gitaly_find_page(title: title, version: version, dir: dir) else @@ -192,7 +195,10 @@ module Gitlab assert_type!(format, Symbol) assert_type!(commit_details, CommitDetails) - gollum_wiki.write_page(name, format, content, commit_details.to_h) + filename = File.basename(name) + dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir + + gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir) nil rescue Gollum::DuplicatePageError => e @@ -210,7 +216,15 @@ module Gitlab assert_type!(format, Symbol) assert_type!(commit_details, CommitDetails) - gollum_wiki.update_page(gollum_page_by_path(page_path), title, format, content, commit_details.to_h) + page = gollum_page_by_path(page_path) + committer = Gollum::Committer.new(page.wiki, commit_details.to_h) + + # Instead of performing two renames if the title has changed, + # the update_page will only update the format and content and + # the rename_page will do anything related to moving/renaming + gollum_wiki.update_page(page, page.name, format, content, committer: committer) + gollum_wiki.rename_page(page, title, committer: committer) + committer.commit nil end diff --git a/lib/gitlab/gitaly_client/wiki_page.rb b/lib/gitlab/gitaly_client/wiki_page.rb index 7339468e911..a02d15db5dd 100644 --- a/lib/gitlab/gitaly_client/wiki_page.rb +++ b/lib/gitlab/gitaly_client/wiki_page.rb @@ -4,6 +4,7 @@ module Gitlab ATTRS = %i(title format url_path path name historical raw_data).freeze include AttributesBag + include Gitlab::EncodingHelper def initialize(params) super @@ -11,6 +12,10 @@ module Gitlab # All gRPC strings in a response are frozen, so we get an unfrozen # version here so appending to `raw_data` doesn't blow up. @raw_data = @raw_data.dup + + @title = encode_utf8(@title) + @path = encode_utf8(@path) + @name = encode_utf8(@name) end def historical? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 94458d60e01..159521a750b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-30 14:59+0100\n" -"PO-Revision-Date: 2018-01-30 14:59+0100\n" +"POT-Creation-Date: 2018-02-05 16:03+0100\n" +"PO-Revision-Date: 2018-02-05 16:03+0100\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -23,6 +23,11 @@ msgid_plural "%d commits" msgstr[0] "" msgstr[1] "" +msgid "%d commit behind" +msgid_plural "%d commits behind" +msgstr[0] "" +msgstr[1] "" + msgid "%d issue" msgid_plural "%d issues" msgstr[0] "" @@ -157,6 +162,18 @@ msgstr "" msgid "An error occurred while fetching sidebar data" msgstr "" +msgid "An error occurred while getting projects" +msgstr "" + +msgid "An error occurred while loading filenames" +msgstr "" + +msgid "An error occurred while rendering KaTeX" +msgstr "" + +msgid "An error occurred while retrieving calendar activity" +msgstr "" + msgid "An error occurred while retrieving diff" msgstr "" @@ -363,7 +380,7 @@ msgstr "" msgid "CI / CD" msgstr "" -msgid "CI configuration" +msgid "CI/CD configuration" msgstr "" msgid "CICD|Jobs" @@ -480,6 +497,24 @@ msgstr "" msgid "CiStatus|running" msgstr "" +msgid "CiVariables|Input variable key" +msgstr "" + +msgid "CiVariables|Input variable value" +msgstr "" + +msgid "CiVariables|Remove variable row" +msgstr "" + +msgid "CiVariable|All environments" +msgstr "" + +msgid "CiVariable|Protected" +msgstr "" + +msgid "CiVariable|Toggle protected" +msgstr "" + msgid "CircuitBreakerApiLink|circuitbreaker api" msgstr "" @@ -791,6 +826,9 @@ msgstr "" msgid "Commits|An error occurred while fetching merge requests data." msgstr "" +msgid "Commits|Commit: %{commitText}" +msgstr "" + msgid "Commits|History" msgstr "" @@ -890,6 +928,9 @@ msgstr "" msgid "Copy URL to clipboard" msgstr "" +msgid "Copy branch name to clipboard" +msgstr "" + msgid "Copy commit SHA to clipboard" msgstr "" @@ -1099,12 +1140,33 @@ msgstr "" msgid "Environments|You don't have any environments right now." msgstr "" +msgid "Error fetching contributors data." +msgstr "" + +msgid "Error fetching labels." +msgstr "" + +msgid "Error fetching network graph." +msgstr "" + msgid "Error fetching refs" msgstr "" +msgid "Error fetching usage ping data." +msgstr "" + msgid "Error occurred when toggling the notification subscription" msgstr "" +msgid "Error saving label update." +msgstr "" + +msgid "Error updating status for all todos." +msgstr "" + +msgid "Error updating todo status." +msgstr "" + msgid "EventFilterBy|Filter by all" msgstr "" @@ -1716,12 +1778,6 @@ msgstr "" msgid "PipelineSchedules|Inactive" msgstr "" -msgid "PipelineSchedules|Input variable key" -msgstr "" - -msgid "PipelineSchedules|Input variable value" -msgstr "" - msgid "PipelineSchedules|Next Run" msgstr "" @@ -1731,9 +1787,6 @@ msgstr "" msgid "PipelineSchedules|Provide a short description for this pipeline" msgstr "" -msgid "PipelineSchedules|Remove variable row" -msgstr "" - msgid "PipelineSchedules|Take ownership" msgstr "" @@ -2087,7 +2140,7 @@ msgstr "" msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" -msgid "Set up CI" +msgid "Set up CI/CD" msgstr "" msgid "Set up Koding" @@ -2116,6 +2169,9 @@ msgstr[1] "" msgid "Snippets" msgstr "" +msgid "Something went wrong on our end" +msgstr "" + msgid "Something went wrong on our end." msgstr "" @@ -2424,6 +2480,18 @@ msgstr "" msgid "There are problems accessing Git storage: " msgstr "" +msgid "There was an error saving your notification settings." +msgstr "" + +msgid "There was an error when reseting email token." +msgstr "" + +msgid "There was an error when subscribing to this label." +msgstr "" + +msgid "There was an error when unsubscribing from this label." +msgstr "" + msgid "This directory" msgstr "" @@ -2635,6 +2703,9 @@ msgstr "" msgid "Trigger this manual action" msgstr "" +msgid "Type %{value} to confirm:" +msgstr "" + msgid "Unable to reset project cache." msgstr "" @@ -2719,6 +2790,12 @@ msgstr "" msgid "WikiClone|Start Gollum and edit locally" msgstr "" +msgid "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title." +msgstr "" + +msgid "WikiEdit|There is already a page with the same title in that path." +msgstr "" + msgid "WikiEmptyPageError|You are not allowed to create wiki pages" msgstr "" @@ -2897,6 +2974,9 @@ msgstr[1] "" msgid "mrWidget|Cancel automatic merge" msgstr "" +msgid "mrWidget|Check out branch" +msgstr "" + msgid "mrWidget|Checking ability to merge automatically" msgstr "" @@ -2906,9 +2986,27 @@ msgstr "" msgid "mrWidget|Cherry-pick this merge request in a new merge request" msgstr "" +msgid "mrWidget|Closed" +msgstr "" + msgid "mrWidget|Closed by" msgstr "" +msgid "mrWidget|Closes" +msgstr "" + +msgid "mrWidget|Did not close" +msgstr "" + +msgid "mrWidget|Email patches" +msgstr "" + +msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the" +msgstr "" + +msgid "mrWidget|Mentions" +msgstr "" + msgid "mrWidget|Merge" msgstr "" @@ -2921,6 +3019,9 @@ msgstr "" msgid "mrWidget|Merged by" msgstr "" +msgid "mrWidget|Plain diff" +msgstr "" + msgid "mrWidget|Refresh" msgstr "" @@ -2936,6 +3037,9 @@ msgstr "" msgid "mrWidget|Remove source branch" msgstr "" +msgid "mrWidget|Request to merge" +msgstr "" + msgid "mrWidget|Resolve conflicts" msgstr "" @@ -2981,9 +3085,18 @@ msgstr "" msgid "mrWidget|This project is archived, write access has been disabled" msgstr "" +msgid "mrWidget|You can merge this merge request manually using the" +msgstr "" + msgid "mrWidget|You can remove source branch now" msgstr "" +msgid "mrWidget|command line" +msgstr "" + +msgid "mrWidget|into" +msgstr "" + msgid "mrWidget|to be merged automatically when the pipeline succeeds" msgstr "" diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index c8cfe251d27..ff3a2a76acc 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -3,36 +3,40 @@ FactoryBot.define do model { build(:project) } size 100.kilobytes uploader "AvatarUploader" + mount_point :avatar + secret nil # we should build a mount agnostic upload by default transient do - mounted_as :avatar - secret SecureRandom.hex + filename 'myfile.jpg' end # this needs to comply with RecordsUpload::Concern#upload_path - path { File.join("uploads/-/system", model.class.to_s.underscore, mounted_as.to_s, 'avatar.jpg') } + path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') } trait :personal_snippet_upload do - model { build(:personal_snippet) } - path { File.join(secret, 'myfile.jpg') } uploader "PersonalFileUploader" + path { File.join(secret, filename) } + model { build(:personal_snippet) } + secret SecureRandom.hex end trait :issuable_upload do - path { File.join(secret, 'myfile.jpg') } uploader "FileUploader" + path { File.join(secret, filename) } + secret SecureRandom.hex end trait :namespace_upload do model { build(:group) } - path { File.join(secret, 'myfile.jpg') } + path { File.join(secret, filename) } uploader "NamespaceFileUploader" + secret SecureRandom.hex end trait :attachment_upload do transient do - mounted_as :attachment + mount_point :attachment end model { build(:note) } diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 949d90a50ff..4d2a08afecc 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' -describe 'User updates wiki page' do +# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages +describe 'User updates wiki page', :skip_gitaly_mock do let(:user) { create(:user) } before do @@ -143,6 +144,7 @@ describe 'User updates wiki page' do expect(page).to have_field('wiki[message]', with: 'Update home') fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Save changes') expect(page).to have_content('Home') @@ -151,4 +153,74 @@ describe 'User updates wiki page' do end end end + + context 'when the page is in a subdir' do + let!(:project) { create(:project, namespace: user.namespace) } + let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } + let(:page_name) { 'page_name' } + let(:page_dir) { "foo/bar/#{page_name}" } + let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) } + + before do + visit(project_wiki_edit_path(project, wiki_page)) + end + + it 'moves the page to the root folder' do + fill_in(:wiki_title, with: "/#{page_name}") + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, page_name)) + end + + it 'moves the page to other dir' do + new_page_dir = "foo1/bar1/#{page_name}" + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end + + it 'remains in the same place if title has not changed' do + original_path = project_wiki_path(project, wiki_page) + + fill_in(:wiki_title, with: page_name) + + click_button('Save changes') + + expect(current_path).to eq(original_path) + end + + it 'can be moved to a different dir with a different name' do + new_page_dir = "foo1/bar1/new_page_name" + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + end + + it 'can be renamed and moved to the root folder' do + new_name = 'new_page_name' + + fill_in(:wiki_title, with: "/#{new_name}") + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, new_name)) + end + + it 'squishes the title before creating the page' do + new_page_dir = " foo1 / bar1 / #{page_name} " + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) + end + end end diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index ff325aeadd3..e37436838fd 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' -describe 'User views a wiki page' do +# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages +describe 'User views a wiki page', :skip_gitaly_mock do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } let(:wiki_page) do diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index 5026eaafaca..2abf52a1676 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -1,10 +1,14 @@ /* eslint-disable no-new */ import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Sidebar from '~/right_sidebar'; +import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Issuable right sidebar collapsed todo toggle', () => { const fixtureName = 'issues/open-issue.html.raw'; const jsonFixtureName = 'todos/todos.json'; + let mock; preloadFixtures(fixtureName); preloadFixtures(jsonFixtureName); @@ -19,19 +23,26 @@ describe('Issuable right sidebar collapsed todo toggle', () => { document.querySelector('.js-right-sidebar') .classList.toggle('right-sidebar-collapsed'); - spyOn(jQuery, 'ajax').and.callFake((res) => { - const d = $.Deferred(); + mock = new MockAdapter(axios); + + mock.onPost(`${gl.TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => { const response = _.clone(todoData); - if (res.type === 'DELETE') { - delete response.delete_path; - } + return [200, response]; + }); - d.resolve(response); - return d.promise(); + mock.onDelete(/(.*)\/dashboard\/todos\/\d+$/).reply(() => { + const response = _.clone(todoData); + delete response.delete_path; + + return [200, response]; }); }); + afterEach(() => { + mock.restore(); + }); + it('shows add todo button', () => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon'), @@ -52,71 +63,101 @@ describe('Issuable right sidebar collapsed todo toggle', () => { ).toBe('Add todo'); }); - it('toggle todo state', () => { + it('toggle todo state', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), - ).not.toBeNull(); + setTimeout(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).not.toBeNull(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'), - ).not.toBeNull(); - }); + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'), + ).not.toBeNull(); - it('toggle todo state of expanded todo toggle', () => { - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - - expect( - document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Mark done'); + done(); + }); }); - it('toggles todo button tooltip', () => { + it('toggle todo state of expanded todo toggle', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), - ).toBe('Mark done'); - }); - - it('marks todo as done', () => { - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + setTimeout(() => { + expect( + document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), + ).toBe('Mark done'); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), - ).not.toBeNull(); + done(); + }); + }); + it('toggles todo button tooltip', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), - ).toBeNull(); + setTimeout(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), + ).toBe('Mark done'); - expect( - document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Add todo'); + done(); + }); }); - it('updates aria-label to mark done', () => { + it('marks todo as done', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + timeoutPromise() + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).not.toBeNull(); + + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + }) + .then(timeoutPromise) + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'), + ).toBeNull(); + + expect( + document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), + ).toBe('Add todo'); + }) + .then(done) + .catch(done.fail); }); - it('updates aria-label to add todo', () => { + it('updates aria-label to mark done', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + setTimeout(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Mark done'); + done(); + }); + }); + + it('updates aria-label to add todo', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); - expect( - document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Add todo'); + timeoutPromise() + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Mark done'); + + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); + }) + .then(timeoutPromise) + .then(() => { + expect( + document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), + ).toBe('Add todo'); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 5245bf6455c..177962ecf82 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -70,23 +70,6 @@ describe('Issue', function() { expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue'); } - describe('task lists', function() { - beforeEach(function() { - loadFixtures('issues/issue-with-task-list.html.raw'); - this.issue = new Issue(); - }); - - it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template - expect(req.data.issue.description).not.toBe(null); - }); - - $('.js-task-list-field').trigger('tasklist:changed'); - }); - }); - [true, false].forEach((isIssueInitiallyOpen) => { describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() { const action = isIssueInitiallyOpen ? 'close' : 'reopen'; diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index bae3219b043..bdfd16ac995 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,5 +1,6 @@ /* eslint-disable space-before-function-paren, no-return-assign */ - +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import MergeRequest from '~/merge_request'; import CloseReopenReportToggle from '~/close_reopen_report_toggle'; import IssuablesHelper from '~/helpers/issuables_helper'; @@ -7,11 +8,24 @@ import IssuablesHelper from '~/helpers/issuables_helper'; (function() { describe('MergeRequest', function() { describe('task lists', function() { + let mock; + preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function() { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + + spyOn(axios, 'patch').and.callThrough(); + mock = new MockAdapter(axios); + + mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {}); + return this.merge = new MergeRequest(); }); + + afterEach(() => { + mock.restore(); + }); + it('modifies the Markdown field', function() { spyOn(jQuery, 'ajax').and.stub(); const changeEvent = document.createEvent('HTMLEvents'); @@ -21,14 +35,14 @@ import IssuablesHelper from '~/helpers/issuables_helper'; }); it('submits an ajax request on tasklist:changed', (done) => { - spyOn(jQuery, 'ajax').and.callFake((req) => { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`); - expect(req.data.merge_request.description).not.toBe(null); + $('.js-task-list-field').trigger('tasklist:changed'); + + setTimeout(() => { + expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, { + merge_request: { description: '- [ ] Task List Item' }, + }); done(); }); - - $('.js-task-list-field').trigger('tasklist:changed'); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2fb385bd79f..274d7591c71 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -50,13 +50,24 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; }); describe('task lists', function() { + let mock; + beforeEach(function() { + spyOn(axios, 'patch').and.callThrough(); + mock = new MockAdapter(axios); + + mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {}); + $('.js-comment-button').on('click', function(e) { e.preventDefault(); }); this.notes = new Notes('', []); }); + afterEach(() => { + mock.restore(); + }); + it('modifies the Markdown field', function() { const changeEvent = document.createEvent('HTMLEvents'); changeEvent.initEvent('change', true, true); @@ -65,14 +76,15 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item'); }); - it('submits an ajax request on tasklist:changed', function() { - spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PATCH'); - expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json'); - return expect(req.data.note).not.toBe(null); - }); + it('submits an ajax request on tasklist:changed', function(done) { + $('.js-task-list-container').trigger('tasklist:changed'); - $('.js-task-list-field.js-note-text').trigger('tasklist:changed'); + setTimeout(() => { + expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, { + note: { note: '' }, + }); + done(); + }); }); }); diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js index b24567ffc0c..f6c0f51cf62 100644 --- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js +++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js @@ -1,3 +1,5 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; import PANEL_STATE from '~/prometheus_metrics/constants'; import { metrics, missingVarMetrics } from './mock_data'; @@ -102,25 +104,38 @@ describe('PrometheusMetrics', () => { describe('loadActiveMetrics', () => { let prometheusMetrics; + let mock; + + function mockSuccess() { + mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, { + data: metrics, + success: true, + }); + } + + function mockError() { + mock.onGet(prometheusMetrics.activeMetricsEndpoint).networkError(); + } beforeEach(() => { + spyOn(axios, 'get').and.callThrough(); + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); }); it('should show loader animation while response is being loaded and hide it when request is complete', (done) => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + mockSuccess(); prometheusMetrics.loadActiveMetrics(); expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); - expect($.ajax).toHaveBeenCalledWith({ - url: prometheusMetrics.activeMetricsEndpoint, - dataType: 'json', - global: false, - }); - - deferred.resolve({ data: metrics, success: true }); + expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); setTimeout(() => { expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); @@ -129,14 +144,10 @@ describe('PrometheusMetrics', () => { }); it('should show empty state if response failed to load', (done) => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - spyOn(prometheusMetrics, 'populateActiveMetrics'); + mockError(); prometheusMetrics.loadActiveMetrics(); - deferred.reject(); - setTimeout(() => { expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); @@ -145,14 +156,11 @@ describe('PrometheusMetrics', () => { }); it('should populate metrics list once response is loaded', (done) => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); spyOn(prometheusMetrics, 'populateActiveMetrics'); + mockSuccess(); prometheusMetrics.loadActiveMetrics(); - deferred.resolve({ data: metrics, success: true }); - setTimeout(() => { expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics); done(); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 3267e29585b..35bb630bf5d 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,6 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ +import MockAdapter from 'axios-mock-adapter'; import '~/commons/bootstrap'; +import axios from '~/lib/utils/axios_utils'; import Sidebar from '~/right_sidebar'; (function() { @@ -35,16 +37,23 @@ import Sidebar from '~/right_sidebar'; var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); loadJSONFixtures('todos/todos.json'); + let mock; beforeEach(function() { loadFixtures(fixtureName); - this.sidebar = new Sidebar; + mock = new MockAdapter(axios); + this.sidebar = new Sidebar(); $aside = $('.right-sidebar'); $page = $('.layout-page'); $icon = $aside.find('i'); $toggle = $aside.find('.js-sidebar-toggle'); return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); }); + + afterEach(() => { + mock.restore(); + }); + it('should expand/collapse the sidebar when arrow is clicked', function() { assertSidebarState('expanded'); $toggle.click(); @@ -63,20 +72,19 @@ import Sidebar from '~/right_sidebar'; return assertSidebarState('collapsed'); }); - it('should broadcast todo:toggle event when add todo clicked', function() { + it('should broadcast todo:toggle event when add todo clicked', function(done) { var todos = getJSONFixture('todos/todos.json'); - spyOn(jQuery, 'ajax').and.callFake(function() { - var d = $.Deferred(); - var response = todos; - d.resolve(response); - return d.promise(); - }); + mock.onPost(/(.*)\/todos$/).reply(200, todos); var todoToggleSpy = spyOnEvent(document, 'todo:toggle'); $('.issuable-sidebar-header .js-issuable-todo').click(); - expect(todoToggleSpy.calls.count()).toEqual(1); + setTimeout(() => { + expect(todoToggleSpy.calls.count()).toEqual(1); + + done(); + }); }); it('should not hide collapsed icons', () => { diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb index c44bc1840df..ebd907ecb7f 100644 --- a/spec/lib/file_size_validator_spec.rb +++ b/spec/lib/file_size_validator_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe FileSizeValidator do let(:validator) { described_class.new(options) } - let(:attachment) { AttachmentUploader.new } let(:note) { create(:note) } + let(:attachment) { AttachmentUploader.new(note) } describe 'options uses an integer' do let(:options) { { maximum: 10, attributes: { attachment: attachment } } } diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb new file mode 100644 index 00000000000..bd8dbf07fa7 --- /dev/null +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::Git::Wiki do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:wiki) { ProjectWiki.new(project, user) } + let(:gollum_wiki) { wiki.wiki } + + # Remove skip_gitaly_mock flag when gitaly_find_page when + # https://gitlab.com/gitlab-org/gitaly/merge_requests/539 gets merged + describe '#page', :skip_gitaly_mock do + it 'returns the right page' do + create_page('page1', 'content') + create_page('foo/page1', 'content') + + expect(gollum_wiki.page(title: 'page1', dir: '').url_path).to eq 'page1' + expect(gollum_wiki.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1' + + destroy_page('page1') + destroy_page('page1', 'foo') + end + end + + def create_page(name, content) + gollum_wiki.write_page(name, :markdown, content, commit_details) + end + + def commit_details + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + end + + def destroy_page(title, dir = '') + page = gollum_wiki.page(title: title, dir: dir) + wiki.delete_page(page, "test commit") + end +end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 929086305ba..1e7671476f1 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -127,7 +127,7 @@ describe ProjectWiki do end after do - destroy_page(subject.pages.first.page) + subject.pages.each { |page| destroy_page(page.page) } end it "returns the latest version of the page if it exists" do @@ -148,6 +148,17 @@ describe ProjectWiki do page = subject.find_page("index page") expect(page).to be_a WikiPage end + + context 'pages with multibyte-character title' do + before do + create_page("autre pagé", "C'est un génial Gollum Wiki") + end + + it "can find a page by slug" do + page = subject.find_page("autre pagé") + expect(page.title).to eq("autre pagé") + end + end end context 'when Gitaly wiki_find_page is enabled' do diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 42f3d609770..0dcaa026332 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -103,4 +103,10 @@ describe Upload do expect(upload).not_to exist end end + + describe "#uploader_context" do + subject { create(:upload, :issuable_upload, secret: 'secret', filename: 'file.txt') } + + it { expect(subject.uploader_context).to match(a_hash_including(secret: 'secret', identifier: 'file.txt')) } + end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 9840afe6c4e..4a1d12cd448 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -188,14 +188,37 @@ describe WikiPage do end end - describe "#update" do + describe '#create', :skip_gitaly_mock do + context 'with valid attributes' do + it 'raises an error if a page with the same path already exists' do + create_page('New Page', 'content') + create_page('foo/bar', 'content') + expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError + expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError + + destroy_page('New Page') + destroy_page('bar', 'foo') + end + + it 'if the title is preceded by a / it is removed' do + create_page('/New Page', 'content') + + expect(wiki.find_page('New Page')).not_to be_nil + + destroy_page('New Page') + end + end + end + + # Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages + describe "#update", :skip_gitaly_mock do before do create_page("Update", "content") @page = wiki.find_page("Update") end after do - destroy_page(@page.title) + destroy_page(@page.title, @page.directory) end context "with valid attributes" do @@ -233,6 +256,95 @@ describe WikiPage do expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError) end end + + context 'when renaming a page' do + it 'raises an error if the page already exists' do + create_page('Existing Page', 'content') + + expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) + expect(@page.title).to eq 'Update' + expect(@page.content).to eq 'new_content' + + destroy_page('Existing Page') + end + + it 'updates the content and rename the file' do + new_title = 'Renamed Page' + new_content = 'updated content' + + expect(@page.update(title: new_title, content: new_content)).to be_truthy + + @page = wiki.find_page(new_title) + + expect(@page).not_to be_nil + expect(@page.content).to eq new_content + end + end + + context 'when moving a page' do + it 'raises an error if the page already exists' do + create_page('foo/Existing Page', 'content') + + expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError) + expect(@page.title).to eq 'Update' + expect(@page.content).to eq 'new_content' + + destroy_page('Existing Page', 'foo') + end + + it 'updates the content and moves the file' do + new_title = 'foo/Other Page' + new_content = 'new_content' + + expect(@page.update(title: new_title, content: new_content)).to be_truthy + + page = wiki.find_page(new_title) + + expect(page).not_to be_nil + expect(page.content).to eq new_content + end + + context 'in subdir' do + before do + create_page('foo/Existing Page', 'content') + @page = wiki.find_page('foo/Existing Page') + end + + it 'moves the page to the root folder if the title is preceded by /' do + expect(@page.slug).to eq 'foo/Existing-Page' + expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy + expect(@page.slug).to eq 'Existing-Page' + end + + it 'does nothing if it has the same title' do + original_path = @page.slug + + expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path + end + end + + context 'in root dir' do + it 'does nothing if the title is preceded by /' do + original_path = @page.slug + + expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy + expect(@page.slug).to eq original_path + end + end + end + + context "with invalid attributes" do + it 'aborts update if title blank' do + expect(@page.update(title: '', content: 'new_content')).to be_falsey + expect(@page.content).to eq 'new_content' + + page = wiki.find_page('Update') + expect(page.content).to eq 'content' + + @page.title = 'Update' + end + end end describe "#destroy" do @@ -421,8 +533,8 @@ describe WikiPage do wiki.wiki.write_page(name, :markdown, content, commit_details) end - def destroy_page(title) - page = wiki.wiki.page(title: title) + def destroy_page(title, dir = '') + page = wiki.wiki.page(title: title, dir: dir) wiki.delete_page(page, "test commit") end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index a72f853df75..a559681a079 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -40,7 +40,7 @@ describe FileUploader do end describe 'initialize' do - let(:uploader) { described_class.new(double, 'secret') } + let(:uploader) { described_class.new(double, secret: 'secret') } it 'accepts a secret parameter' do expect(described_class).not_to receive(:generate_secret) @@ -54,4 +54,31 @@ describe FileUploader do expect(uploader.secret).to eq('secret') end end + + describe '#upload=' do + let(:secret) { SecureRandom.hex } + let(:upload) { create(:upload, :issuable_upload, secret: secret, filename: 'file.txt') } + + it 'handles nil' do + expect(uploader).not_to receive(:apply_context!) + + uploader.upload = nil + end + + it 'extract the uploader context from it' do + expect(uploader).to receive(:apply_context!).with(a_hash_including(secret: secret, identifier: 'file.txt')) + + uploader.upload = upload + end + + context 'uploader_context is empty' do + it 'fallbacks to regex based extraction' do + expect(upload).to receive(:uploader_context).and_return({}) + + uploader.upload = upload + expect(uploader.secret).to eq(secret) + expect(uploader.instance_variable_get(:@identifier)).to eq('file.txt') + end + end + end end diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb index a144b39f74f..60e35dcf235 100644 --- a/spec/uploaders/gitlab_uploader_spec.rb +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -4,7 +4,7 @@ require 'carrierwave/storage/fog' describe GitlabUploader do let(:uploader_class) { Class.new(described_class) } - subject { uploader_class.new } + subject { uploader_class.new(double) } describe '#file_storage?' do context 'when file storage is used' do |