diff options
author | Clement Ho <clemmakesapps@gmail.com> | 2017-06-26 16:29:16 +0300 |
---|---|---|
committer | Clement Ho <clemmakesapps@gmail.com> | 2017-06-26 16:29:16 +0300 |
commit | ca2c2dbb94c6ade0e220ae5085c7af690d181b10 (patch) | |
tree | 0a0a23f3d07bdb0bb57b6aaf1f893646f8cd2e6d | |
parent | acb7f257db65e8ce2907e4de7c42627c110476e1 (diff) | |
parent | 74b031e58221a4f402d318814b614b7395a569a9 (diff) |
Merge branch '9-3-stable-patch-1' into '9-3-stable'
Prepare 9.3.1
See merge request !12404
51 files changed, 379 insertions, 126 deletions
@@ -2,7 +2,6 @@ source 'https://rubygems.org' gem 'rails', '4.2.8' gem 'rails-deprecated_sanitizer', '~> 1.0.3' -gem 'bootsnap', '~> 1.0.0' # Responders respond_to and respond_with gem 'responders', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 676cd977e37..83f6961e785 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,8 +83,6 @@ GEM bindata (2.3.5) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootsnap (1.0.0) - msgpack (~> 1.0) bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) @@ -464,7 +462,6 @@ GEM minitest (5.7.0) mmap2 (2.2.6) mousetrap-rails (1.4.6) - msgpack (1.1.0) multi_json (1.12.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -924,7 +921,6 @@ DEPENDENCIES benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) binding_of_caller (~> 0.7.2) - bootsnap (~> 1.0.0) bootstrap-sass (~> 3.3.0) brakeman (~> 3.6.0) browser (~> 2.2) diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index de2269118cd..9b9e83a54f1 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -403,6 +403,14 @@ export default { return ''; }, + displayEnvironmentActions() { + return this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.hasStopAction || + this.canRetry; + }, + /** * Constructs folder URL based on the current location and the folder id. * @@ -535,10 +543,13 @@ export default { </span> </div> - <div class="table-section section-30 environments-actions table-button-footer" role="gridcell"> + <div + v-if="!model.isFolder && displayEnvironmentActions" + class="table-section section-30 table-button-footer" + role="gridcell"> + <div - v-if="!model.isFolder" - class="btn-group environment-action-buttons" + class="btn-group table-action-buttons" role="group"> <actions-component diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 67ee7d140ce..078dae6cc65 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -47,8 +47,8 @@ export default class GroupsStore { // Map groups to an object groups.map((group) => { - mappedGroups[group.id] = group; - mappedGroups[group.id].subGroups = {}; + mappedGroups[`id${group.id}`] = group; + mappedGroups[`id${group.id}`].subGroups = {}; return group; }); @@ -56,26 +56,27 @@ export default class GroupsStore { const currentGroup = mappedGroups[key]; if (currentGroup.parentId) { // If the group is not at the root level, add it to its parent array of subGroups. - const findParentGroup = mappedGroups[currentGroup.parentId]; + const findParentGroup = mappedGroups[`id${currentGroup.parentId}`]; if (findParentGroup) { - mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; - mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups + mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup; + mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups } else if (parentGroup && parentGroup.id === currentGroup.parentId) { - tree[currentGroup.id] = currentGroup; + tree[`id${currentGroup.id}`] = currentGroup; } else { - // Means the groups hast no direct parent. - // Save for later processing, we will add them to its corresponding base group + // No parent found. We save it for later processing orphans.push(currentGroup); + + // Add to tree to preserve original order + tree[`id${currentGroup.id}`] = currentGroup; } } else { - // If the group is at the root level, add it to first level elements array. - tree[currentGroup.id] = currentGroup; + // If the group is at the top level, add it to first level elements array. + tree[`id${currentGroup.id}`] = currentGroup; } return key; }); - // Hopefully this array will be empty for most cases if (orphans.length) { orphans.map((orphan) => { let found = false; @@ -83,11 +84,23 @@ export default class GroupsStore { Object.keys(tree).map((key) => { const group = tree[key]; - if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) { + + if ( + group && + currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 && + // Make sure the currently selected orphan is not the same as the group + // we are checking here otherwise it will end up in an infinite loop + currentOrphan.id !== group.id + ) { group.subGroups[currentOrphan.id] = currentOrphan; group.isOpen = true; currentOrphan.isOrphan = true; found = true; + + // Delete if group was put at the top level. If not the group will be displayed twice. + if (tree[`id${currentOrphan.id}`]) { + delete tree[`id${currentOrphan.id}`]; + } } return key; @@ -95,7 +108,8 @@ export default class GroupsStore { if (!found) { currentOrphan.isOrphan = true; - tree[currentOrphan.id] = currentOrphan; + + tree[`id${currentOrphan.id}`] = currentOrphan; } return orphan; @@ -139,7 +153,7 @@ export default class GroupsStore { // eslint-disable-next-line class-methods-use-this removeGroup(group, collection) { - Vue.delete(collection, group.id); + Vue.delete(collection, `id${group.id}`); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 8473a81bc88..3d5fb7f441c 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -204,13 +204,7 @@ export default { method: 'getData', successCallback: (res) => { const data = res.json(); - const shouldUpdate = this.store.stateShouldUpdate(data); - this.store.updateState(data); - - if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) { - this.store.formState.lockedWarningVisible = true; - } }, errorCallback(err) { throw new Error(err); diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 30a1be5cb50..4835fc39ea7 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -47,7 +47,8 @@ ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." - @keydown.meta.enter="updateIssuable"> + @keydown.meta.enter="updateIssuable" + @keydown.ctrl.enter="updateIssuable"> </textarea> </markdown-field> </div> diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index 6556bf117e2..83af8e1e245 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -26,6 +26,7 @@ placeholder="Issue title" aria-label="Issue title" v-model="formState.title" - @keydown.meta.enter="updateIssuable" /> + @keydown.meta.enter="updateIssuable" + @keydown.ctrl.enter="updateIssuable" /> </fieldset> </template> diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index f2b822f3cbb..0c8bd6f1cc3 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -12,6 +12,10 @@ export default class Store { } updateState(data) { + if (this.stateShouldUpdate(data)) { + this.formState.lockedWarningVisible = true; + } + this.state.titleHtml = data.title; this.state.titleText = data.title_text; this.state.descriptionHtml = data.description; @@ -23,10 +27,8 @@ export default class Store { } stateShouldUpdate(data) { - return { - title: this.state.titleText !== data.title_text, - description: this.state.descriptionText !== data.description_text, - }; + return this.state.titleText !== data.title_text || + this.state.descriptionText !== data.description_text; } setFormState(state) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2aca86189fd..122ec138c59 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -86,18 +86,25 @@ // This is required to handle non-unicode characters in hash hash = decodeURIComponent(hash); + var fixedTabs = document.querySelector('.js-tabs-affix'); + var fixedNav = document.querySelector('.navbar-gitlab'); + + var adjustment = 0; + if (fixedNav) adjustment -= fixedNav.offsetHeight; + // scroll to user-generated markdown anchor if we cannot find a match if (document.getElementById(hash) === null) { var target = document.getElementById('user-content-' + hash); if (target && target.scrollIntoView) { target.scrollIntoView(true); + window.scrollBy(0, adjustment); } } else { // only adjust for fixedTabs when not targeting user-generated content - var fixedTabs = document.querySelector('.js-tabs-affix'); if (fixedTabs) { - window.scrollBy(0, -fixedTabs.offsetHeight); + adjustment -= fixedTabs.offsetHeight; } + window.scrollBy(0, adjustment); } }; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 894ed81b044..70340af9bea 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -155,7 +155,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; scrollToElement(container) { if (location.hash) { - const offset = -$('.js-tabs-affix').outerHeight(); + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { offset }); @@ -284,7 +287,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Scroll any linked note into view // Similar to `toggler_behavior` in the discussion tab const hash = window.gl.utils.getLocationHash(); - const anchor = hash && $container.find(`[id="${hash}"]`); + const anchor = hash && $container.find(`.note[id="${hash}"]`); if (anchor && anchor.length > 0) { const notesContent = anchor.closest('.notes_content'); const lineType = notesContent.hasClass('new') ? 'new' : 'old'; @@ -294,6 +297,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; forceShow: true, }); anchor[0].scrollIntoView(); + window.gl.utils.handleLocationHash(); // We have multiple elements on the page with `#note_xxx` // (discussion and diff tabs) and `:target` only applies to the first anchor.addClass('target'); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index da7c0c5a36c..322162afdb8 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -10,8 +10,6 @@ import Cookies from 'js-cookie'; this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); this.$navGitlab = $('.navbar-gitlab'); - this.$layoutNav = $('.layout-nav'); - this.$subScroll = $('.sub-nav-scroll'); this.$rightSidebar = $('.js-right-sidebar'); this.removeListeners(); @@ -215,7 +213,7 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); + const $navHeight = this.$navGitlab.outerHeight(); const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { this.$rightSidebar.outerHeight($(window).height() - diff); diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 59ff2a86293..7fa5996d600 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -4,7 +4,7 @@ function expandSectionParent($section, $content) { } function expandSection($section) { - $section.find('.js-settings-toggle').text('Close'); + $section.find('.js-settings-toggle').text('Collapse'); const $content = $section.find('.settings-content'); $content.addClass('expanded').off('scroll.expandSection').scrollTop(0); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index ec45253e50b..46efdcf4202 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -643,7 +643,7 @@ UsersSelect.prototype.formatResult = function(user) { } else { avatar = gon.default_avatar_url; } - return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>"; + return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + ("@" + user.username || "") + "</div> </div>"; }; UsersSelect.prototype.formatSelection = function(user) { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cba890ce831..4f54ca24940 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -395,6 +395,11 @@ .dropdown-menu-align-right { left: auto; right: 0; + margin-top: -5px; + + @media (max-width: $screen-xs-max) { + left: 0; + } } .dropdown-menu-selectable { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index d8645afb7da..94986badaf9 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -346,6 +346,7 @@ header { width: auto; min-width: 140px; margin-top: -5px; + left: auto; .current-user { padding: 5px 18px; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index b24803678ea..d820ca198c3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -83,6 +83,7 @@ .avatar { float: none; + margin-right: 0; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2dc7f73a295..59e0624d94e 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -419,7 +419,7 @@ .commit { margin: 0; - padding: 10px 0; + padding: 10px; list-style: none; &:hover { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 410b22224aa..7eda020e4e9 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -174,7 +174,7 @@ overflow: hidden; display: inline-block; white-space: nowrap; - vertical-align: top; + vertical-align: middle; text-overflow: ellipsis; } diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index c003b01e226..eb45241615f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -15,7 +15,7 @@ module GroupsHelper @has_group_title = true full_title = '' - group.ancestors.each do |parent| + group.ancestors.reverse.each do |parent| full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') full_title += '<span class="hidable"> / </span>'.html_safe end diff --git a/app/models/project.rb b/app/models/project.rb index 36ec4f398ca..e1ee22b1a82 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -350,7 +350,10 @@ class Project < ActiveRecord::Base project.run_after_commit { add_import_job } end - after_transition started: :finished, do: :reset_cache_and_import_attrs + after_transition started: :finished do |project, _| + project.reset_cache_and_import_attrs + project.perform_housekeeping + end end class << self @@ -510,6 +513,18 @@ class Project < ActiveRecord::Base remove_import_data end + def perform_housekeeping + return unless repo_exists? + + run_after_commit do + begin + Projects::HousekeepingService.new(self).execute + rescue Projects::HousekeepingService::LeaseTaken => e + Rails.logger.info("Could not perform housekeeping for project #{self.path_with_namespace} (#{self.id}): #{e}") + end + end + end + def remove_import_data import_data&.destroy end diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index c7302414386..969c423b032 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -4,7 +4,7 @@ = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul - - if @group + - if @group&.persisted? - create_group_project = can?(current_user, :create_projects, @group) - create_group_subgroup = can?(current_user, :create_subgroup, @group) - if create_group_project || create_group_subgroup @@ -18,7 +18,7 @@ %li.divider %li.dropdown-bold-header GitLab - - if @project && @project.persisted? + - if @project&.persisted? - create_project_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - create_project_snippet = can?(current_user, :create_project_snippet, @project) diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 6e038ffd9c0..cb98ce04430 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -4,7 +4,7 @@ %h4 Deploy Keys %button.btn.js-settings-toggle - = expanded ? 'Close' : 'Expand' + = expanded ? 'Collapse' : 'Expand' %p Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. .settings-content.no-animate{ class: ('expanded' if expanded) } diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index d956cb2cc1a..b3052bf7c35 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -3,18 +3,19 @@ .table-mobile-header{ role: 'rowheader' } ID %strong.table-mobile-content ##{deployment.iid} - .table-section.section-40{ role: 'gridcell' } + .table-section.section-30{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' } Commit = render 'projects/deployments/commit', deployment: deployment - .table-section.section-15.build-column{ role: 'gridcell' } + .table-section.section-25.build-column{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' } Job - if deployment.deployable - = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link table-mobile-content' do - #{deployment.deployable.name} (##{deployment.deployable.id}) - - if deployment.user - by - = user_avatar(user: deployment.user, size: 20) + .table-mobile-content + = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do + #{deployment.deployable.name} (##{deployment.deployable.id}) + - if deployment.user + by + = user_avatar(user: deployment.user, size: 20) .table-section.section-15{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' } Created diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml index 9af67649741..5d2422bdf54 100644 --- a/app/views/projects/protected_branches/_index.html.haml +++ b/app/views/projects/protected_branches/_index.html.haml @@ -7,7 +7,7 @@ %h4 Protected Branches %button.btn.js-settings-toggle - = expanded ? 'Close' : 'Expand' + = expanded ? 'Collapse' : 'Expand' %p Keep stable branches secure and force developers to use merge requests. .settings-content.no-animate{ class: ('expanded' if expanded) } diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml index 976e1d7e93f..8250f692a69 100644 --- a/app/views/projects/protected_tags/_index.html.haml +++ b/app/views/projects/protected_tags/_index.html.haml @@ -7,7 +7,7 @@ %h4 Protected Tags %button.btn.js-settings-toggle - = expanded ? 'Close' : 'Expand' + = expanded ? 'Collapse' : 'Expand' %p Limit access to creating and updating tags. .settings-content.no-animate{ class: ('expanded' if expanded) } diff --git a/changelogs/unreleased/34010-fix-linking-to-parallel-diff-line-number-creating-gray-box.yml b/changelogs/unreleased/34010-fix-linking-to-parallel-diff-line-number-creating-gray-box.yml new file mode 100644 index 00000000000..705d44f8b10 --- /dev/null +++ b/changelogs/unreleased/34010-fix-linking-to-parallel-diff-line-number-creating-gray-box.yml @@ -0,0 +1,4 @@ +--- +title: Fix linking to line number on side-by-side diff creating empty discussion box +merge_request: +author: diff --git a/changelogs/unreleased/dm-requirements-txt-tilde.yml b/changelogs/unreleased/dm-requirements-txt-tilde.yml new file mode 100644 index 00000000000..ddb5325ddd5 --- /dev/null +++ b/changelogs/unreleased/dm-requirements-txt-tilde.yml @@ -0,0 +1,5 @@ +--- +title: Don't match tilde and exclamation mark as part of requirements.txt package + name +merge_request: +author: diff --git a/changelogs/unreleased/fix-34019.yml b/changelogs/unreleased/fix-34019.yml new file mode 100644 index 00000000000..77ebb18fda6 --- /dev/null +++ b/changelogs/unreleased/fix-34019.yml @@ -0,0 +1,4 @@ +--- +title: Perform project housekeeping after importing projects +merge_request: +author: diff --git a/changelogs/unreleased/issue-inline-edit-quick-submit.yml b/changelogs/unreleased/issue-inline-edit-quick-submit.yml new file mode 100644 index 00000000000..4407bae3e6f --- /dev/null +++ b/changelogs/unreleased/issue-inline-edit-quick-submit.yml @@ -0,0 +1,4 @@ +--- +title: Fixed ctrl+enter not submit issue edit form +merge_request: +author: diff --git a/changelogs/unreleased/mk-fix-breadcrumb-order-33938.yml b/changelogs/unreleased/mk-fix-breadcrumb-order-33938.yml new file mode 100644 index 00000000000..790af692b92 --- /dev/null +++ b/changelogs/unreleased/mk-fix-breadcrumb-order-33938.yml @@ -0,0 +1,4 @@ +--- +title: Fix reversed breadcrumb order for nested groups +merge_request: 12322 +author: diff --git a/changelogs/unreleased/mk-fix-issue-34068.yml b/changelogs/unreleased/mk-fix-issue-34068.yml new file mode 100644 index 00000000000..af0a9139568 --- /dev/null +++ b/changelogs/unreleased/mk-fix-issue-34068.yml @@ -0,0 +1,4 @@ +--- +title: Fix 500 when failing to create private group +merge_request: 12394 +author: diff --git a/config/boot.rb b/config/boot.rb index 16de55d7a86..2d01092acd5 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -9,15 +9,3 @@ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) if ENV['RAILS_ENV'] == 'development' || ENV['RAILS_ENV'] == 'test' ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' end - -# Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage -require 'bootsnap' -Bootsnap.setup( - cache_dir: 'tmp/cache', - development_mode: ENV['RAILS_ENV'] == 'development', - load_path_cache: true, - autoload_paths_cache: true, - disable_trace: false, - compile_cache_iseq: true, - compile_cache_yaml: true -) diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb new file mode 100644 index 00000000000..0fee832788d --- /dev/null +++ b/config/initializers/flipper.rb @@ -0,0 +1,4 @@ +require 'flipper/middleware/memoizer' + +Rails.application.config.middleware.use Flipper::Middleware::Memoizer, + lambda { Feature.flipper } diff --git a/config/webpack.config.js b/config/webpack.config.js index b84d6bb38d0..e643635270a 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -11,22 +11,13 @@ var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeMod var ROOT_PATH = path.resolve(__dirname, '..'); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; -var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; +var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var NO_COMPRESSION = process.env.NO_COMPRESSION; -// optional dependency `node-zopfli` is unavailable on CentOS 6 -var ZOPFLI_AVAILABLE; -try { - require.resolve('node-zopfli'); - ZOPFLI_AVAILABLE = true; -} catch(err) { - ZOPFLI_AVAILABLE = false; -} - var config = { // because sqljs requires fs. node: { @@ -231,12 +222,12 @@ if (IS_PRODUCTION) { // zopfli requires a lot of compute time and is disabled in CI if (!NO_COMPRESSION) { - config.plugins.push( - new CompressionPlugin({ - asset: '[path].gz[query]', - algorithm: ZOPFLI_AVAILABLE ? 'zopfli' : 'gzip', - }) - ); + // gracefully fall back to gzip if `node-zopfli` is unavailable (e.g. in CentOS 6) + try { + config.plugins.push(new CompressionPlugin({ algorithm: 'zopfli' })); + } catch(err) { + config.plugins.push(new CompressionPlugin({ algorithm: 'gzip' })); + } } } diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 41cae58782d..88e53ff40e8 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -155,7 +155,7 @@ Find more information about different Runners in the [Runners](../runners/README.md) documentation. You can find whether any Runners are assigned to your project by going to -**Settings ➔ CI/CD Pipelines**. Setting up a Runner is easy and straightforward. The +**Settings ➔ Pipelines**. Setting up a Runner is easy and straightforward. The official Runner supported by GitLab is written in Go and its documentation can be found at <https://docs.gitlab.com/runner/>. @@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as described in the next section. Once the Runner has been set up, you should see it on the Runners page of your -project, following **Settings ➔ CI/CD Pipelines**. +project, following **Settings ➔ Pipelines**. ![Activated runners](img/runners_activated.png) @@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can build any project. To enable the **Shared Runners** you have to go to your project's -**Settings ➔ CI/CD Pipelines** and click **Enable shared runners**. +**Settings ➔ Pipelines** and click **Enable shared runners**. [Read more on Shared Runners](../runners/README.md). diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 1d2eba4f74b..a992a348c0f 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -1,7 +1,7 @@ # Pipelines settings To reach the pipelines settings navigate to your project's -**Settings ➔ CI/CD Pipelines**. +**Settings ➔ Pipelines**. The following settings can be configured per project. diff --git a/lib/feature.rb b/lib/feature.rb index 5650a1c1334..d3d972564af 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -39,8 +39,6 @@ class Feature get(key).disable end - private - def flipper @flipper ||= begin adapter = Flipper::Adapters::ActiveRecord.new( diff --git a/lib/gitlab/dependency_linker/requirements_txt_linker.rb b/lib/gitlab/dependency_linker/requirements_txt_linker.rb index 2e197e5cd94..9c9620bc36a 100644 --- a/lib/gitlab/dependency_linker/requirements_txt_linker.rb +++ b/lib/gitlab/dependency_linker/requirements_txt_linker.rb @@ -6,7 +6,7 @@ module Gitlab private def link_dependencies - link_regex(/^(?<name>(?![a-z+]+:)[^#.-][^ ><=;\[]+)/) do |name| + link_regex(/^(?<name>(?![a-z+]+:)[^#.-][^ ><=~!;\[]+)/) do |name| "https://pypi.python.org/pypi/#{name}" end diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 88ad760bea3..a42f4b8340d 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -319,7 +319,7 @@ module Gitlab end def init_from_gitaly(diff) - @diff = diff.patch if diff.respond_to?(:patch) + @diff = encode!(diff.patch) if diff.respond_to?(:patch) @new_path = encode!(diff.to_path.dup) @old_path = encode!(diff.from_path.dup) @a_mode = diff.old_mode.to_s(8) diff --git a/lib/gitlab/gitaly_client/diff_stitcher.rb b/lib/gitlab/gitaly_client/diff_stitcher.rb index d84e8d752dc..65d81dc5d46 100644 --- a/lib/gitlab/gitaly_client/diff_stitcher.rb +++ b/lib/gitlab/gitaly_client/diff_stitcher.rb @@ -13,7 +13,10 @@ module Gitlab @rpc_response.each do |diff_msg| if current_diff.nil? diff_params = diff_msg.to_h.slice(*GitalyClient::Diff::FIELDS) - diff_params[:patch] = diff_msg.raw_patch_data + # gRPC uses frozen strings by default, and we need to have an unfrozen string as it + # gets processed further down the line. So we unfreeze the first chunk of the patch + # in case it's the only chunk we receive for this diff. + diff_params[:patch] = diff_msg.raw_patch_data.dup current_diff = GitalyClient::Diff.new(diff_params) else diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 613b1edba36..cceed34ec90 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -151,7 +151,7 @@ feature 'Environments page', :feature, :js do find('.js-dropdown-play-icon-container').click expect(page).to have_content(action.name.humanize) - expect { find('.js-manual-action-link').click } + expect { find('.js-manual-action-link').trigger('click') } .not_to change { Ci::Pipeline.count } end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 0337afa4452..25388a56b57 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe GroupsHelper do + include ApplicationHelper + describe 'group_icon' do avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') @@ -81,4 +83,15 @@ describe GroupsHelper do end end end + + describe 'group_title' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'outputs the groups in the correct order' do + expect(group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) + end + end end diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 0715f4d5f6b..daaddd8f390 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -55,20 +55,27 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end - it 'merge_requests/changes_tab_with_comments.json' do |example| + it 'merge_requests/inline_changes_tab_with_comments.json' do |example| create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) render_merge_request(example.description, merge_request, action: :diffs, format: :json) end + it 'merge_requests/parallel_changes_tab_with_comments.json' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request, action: :diffs, format: :json, view: 'parallel') + end + private - def render_merge_request(fixture_file_name, merge_request, action: :show, format: :html) + def render_merge_request(fixture_file_name, merge_request, action: :show, format: :html, view: 'inline') get action, namespace_id: project.namespace.to_param, project_id: project, id: merge_request.to_param, - format: format + format: format, + view: view expect(response).to be_success store_frontend_fixture(response, fixture_file_name) diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js index 2a77f7259da..aaffb56fa94 100644 --- a/spec/javascripts/groups/groups_spec.js +++ b/spec/javascripts/groups/groups_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import eventHub from '~/groups/event_hub'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; import groupsComponent from '~/groups/components/groups.vue'; @@ -46,6 +47,12 @@ describe('Groups Component', () => { expect(component.$el.querySelector('#group-1120')).toBeDefined(); }); + it('should respect the order of groups', () => { + const wrap = component.$el.querySelector('.groups-list-tree-container > .group-list-tree'); + expect(wrap.querySelector('.group-row:nth-child(1)').id).toBe('group-12'); + expect(wrap.querySelector('.group-row:nth-child(2)').id).toBe('group-1119'); + }); + it('should render group and its subgroup', () => { const lists = component.$el.querySelectorAll('.group-list-tree'); @@ -54,11 +61,26 @@ describe('Groups Component', () => { expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true); expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true); - expect(lists[2].querySelector('#group-1120').textContent).toContain(groups[1119].subGroups[1120].name); + expect(lists[2].querySelector('#group-1120').textContent).toContain(groups.id1119.subGroups.id1120.name); }); it('should remove prefix of parent group', () => { expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4'); }); + + it('should remove the group after leaving the group', (done) => { + spyOn(window, 'confirm').and.returnValue(true); + + eventHub.$on('leaveGroup', (group, collection) => { + store.removeGroup(group, collection); + }); + + component.$el.querySelector('#group-12 .leave-group').click(); + + Vue.nextTick(() => { + expect(component.$el.querySelector('#group-12')).toBeNull(); + done(); + }); + }); }); }); diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js index f5b35b1e8b0..df8189d9290 100644 --- a/spec/javascripts/issue_show/components/fields/description_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_spec.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import eventHub from '~/issue_show/event_hub'; import Store from '~/issue_show/stores'; import descriptionField from '~/issue_show/components/fields/description.vue'; +import { keyboardDownEvent } from '../../helpers'; describe('Description field component', () => { let vm; @@ -18,6 +20,8 @@ describe('Description field component', () => { document.body.appendChild(el); + spyOn(eventHub, '$emit'); + vm = new Component({ el, propsData: { @@ -53,4 +57,20 @@ describe('Description field component', () => { document.activeElement, ).toBe(vm.$refs.textarea); }); + + it('triggers update with meta+enter', () => { + vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); + + it('triggers update with ctrl+enter', () => { + vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); }); diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js index 53ae038a6a2..a03b462689f 100644 --- a/spec/javascripts/issue_show/components/fields/title_spec.js +++ b/spec/javascripts/issue_show/components/fields/title_spec.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import eventHub from '~/issue_show/event_hub'; import Store from '~/issue_show/stores'; import titleField from '~/issue_show/components/fields/title.vue'; +import { keyboardDownEvent } from '../../helpers'; describe('Title field component', () => { let vm; @@ -15,6 +17,8 @@ describe('Title field component', () => { }); store.formState.title = 'test'; + spyOn(eventHub, '$emit'); + vm = new Component({ propsData: { formState: store.formState, @@ -27,4 +31,20 @@ describe('Title field component', () => { vm.$el.querySelector('.form-control').value, ).toBe('test'); }); + + it('triggers update with meta+enter', () => { + vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); + + it('triggers update with ctrl+enter', () => { + vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect( + eventHub.$emit, + ).toHaveBeenCalled(); + }); }); diff --git a/spec/javascripts/issue_show/helpers.js b/spec/javascripts/issue_show/helpers.js new file mode 100644 index 00000000000..5d2ced98ae4 --- /dev/null +++ b/spec/javascripts/issue_show/helpers.js @@ -0,0 +1,10 @@ +// eslint-disable-next-line import/prefer-default-export +export const keyboardDownEvent = (code, metaKey = false, ctrlKey = false) => { + const e = new CustomEvent('keydown'); + + e.keyCode = code; + e.metaKey = metaKey; + e.ctrlKey = ctrlKey; + + return e; +}; diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index b6d0ce02c4f..9e9eb17d439 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -15,7 +15,7 @@ describe('Merge request notes', () => { gl.utils = gl.utils || {}; const discussionTabFixture = 'merge_requests/diff_comment.html.raw'; - const changesTabJsonFixture = 'merge_requests/changes_tab_with_comments.json'; + const changesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; preloadFixtures(discussionTabFixture, changesTabJsonFixture); describe('Discussion tab with diff comments', () => { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7b910282cc8..5c32e7bbd38 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -31,7 +31,15 @@ import 'vendor/jquery.scrollTo'; }; $.extend(stubLocation, defaults, stubs || {}); }; - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw', 'merge_requests/diff_comment.html.raw'); + + const inlineChangesTabJsonFixture = 'merge_requests/inline_changes_tab_with_comments.json'; + const parallelChangesTabJsonFixture = 'merge_requests/parallel_changes_tab_with_comments.json'; + preloadFixtures( + 'merge_requests/merge_request_with_task_list.html.raw', + 'merge_requests/diff_comment.html.raw', + inlineChangesTabJsonFixture, + parallelChangesTabJsonFixture + ); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -284,6 +292,19 @@ import 'vendor/jquery.scrollTo'; }); describe('loadDiff', function () { + beforeEach(() => { + loadFixtures('merge_requests/diff_comment.html.raw'); + spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); + window.gl.ImageFile = () => {}; + window.notes = new Notes('', []); + spyOn(window.notes, 'toggleDiffNote').and.callThrough(); + }); + + afterEach(() => { + delete window.gl.ImageFile; + delete window.notes; + }); + it('requires an absolute pathname', function () { spyOn($, 'ajax').and.callFake(function (options) { expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json'); @@ -292,43 +313,112 @@ import 'vendor/jquery.scrollTo'; this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); }); - describe('with note fragment hash', () => { + describe('with inline diff', () => { + let noteId; + let noteLineNumId; + beforeEach(() => { - loadFixtures('merge_requests/diff_comment.html.raw'); - spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); - window.notes = new Notes('', []); - spyOn(window.notes, 'toggleDiffNote').and.callThrough(); - }); + const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture); + + const $html = $(diffsResponse.html); + noteId = $html.find('.note').attr('id'); + noteLineNumId = $html + .find('.note') + .closest('.notes_holder') + .prev('.line_holder') + .find('a[data-linenumber]') + .attr('href') + .replace('#', ''); - afterEach(() => { - delete window.notes; + spyOn($, 'ajax').and.callFake(function (options) { + options.success(diffsResponse); + }); }); - it('should expand and scroll to linked fragment hash #note_xxx', function () { - const noteId = 'note_1'; - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ html: `<div id="${noteId}">foo</div>` }); + describe('with note fragment hash', () => { + it('should expand and scroll to linked fragment hash #note_xxx', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(noteId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'old', + forceShow: true, + }); }); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + it('should gracefully ignore non-existant fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'old', - forceShow: true, + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); }); }); - it('should gracefully ignore non-existant fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + describe('with line number fragment hash', () => { + it('should gracefully ignore line number fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(noteLineNumId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); + }); + }); + + describe('with parallel diff', () => { + let noteId; + let noteLineNumId; + + beforeEach(() => { + const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture); + + const $html = $(diffsResponse.html); + noteId = $html.find('.note').attr('id'); + noteLineNumId = $html + .find('.note') + .closest('.notes_holder') + .prev('.line_holder') + .find('a[data-linenumber]') + .attr('href') + .replace('#', ''); + spyOn($, 'ajax').and.callFake(function (options) { - options.success({ html: '' }); + options.success(diffsResponse); }); + }); + + describe('with note fragment hash', () => { + it('should expand and scroll to linked fragment hash #note_xxx', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); - this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + expect(noteId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'new', + forceShow: true, + }); + }); + + it('should gracefully ignore non-existant fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); + }); + + describe('with line number fragment hash', () => { + it('should gracefully ignore line number fragment hash', function () { + spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); + + expect(noteLineNumId.length).toBeGreaterThan(0); + expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb index 4da8821726c..7e32770f95d 100644 --- a/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb +++ b/spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb @@ -54,6 +54,8 @@ describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do Sphinx>=1.3 docutils>=0.7 markupsafe + pytest~=3.0 + foop!=3.0 CONTENT end @@ -78,6 +80,8 @@ describe Gitlab::DependencyLinker::RequirementsTxtLinker, lib: true do expect(subject).to include(link('Sphinx', 'https://pypi.python.org/pypi/Sphinx')) expect(subject).to include(link('docutils', 'https://pypi.python.org/pypi/docutils')) expect(subject).to include(link('markupsafe', 'https://pypi.python.org/pypi/markupsafe')) + expect(subject).to include(link('pytest', 'https://pypi.python.org/pypi/pytest')) + expect(subject).to include(link('foop', 'https://pypi.python.org/pypi/foop')) end it 'links URLs' do diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index da213f617cc..28816778620 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -175,6 +175,14 @@ EOT expect(diff).to be_too_large end end + + context 'when the patch passed is not UTF-8-encoded' do + let(:raw_patch) { @raw_diff_hash[:diff].encode(Encoding::ASCII_8BIT) } + + it 'encodes diff patch to UTF-8' do + expect(diff.diff.encoding).to eq(Encoding::UTF_8) + end + end end end |