Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClement Ho <clemmakesapps@gmail.com>2017-06-26 16:29:16 +0300
committerClement Ho <clemmakesapps@gmail.com>2017-06-26 16:29:16 +0300
commitca2c2dbb94c6ade0e220ae5085c7af690d181b10 (patch)
tree0a0a23f3d07bdb0bb57b6aaf1f893646f8cd2e6d
parentacb7f257db65e8ce2907e4de7c42627c110476e1 (diff)
parent74b031e58221a4f402d318814b614b7395a569a9 (diff)
Merge branch '9-3-stable-patch-1' into '9-3-stable'
Prepare 9.3.1 See merge request !12404
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue17
-rw-r--r--app/assets/javascripts/groups/stores/groups_store.js42
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue3
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue3
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js10
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js11
-rw-r--r--app/assets/javascripts/merge_request_tabs.js8
-rw-r--r--app/assets/javascripts/right_sidebar.js4
-rw-r--r--app/assets/javascripts/settings_panels.js2
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/pages/environments.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss2
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/models/project.rb17
-rw-r--r--app/views/layouts/header/_new_dropdown.haml4
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml15
-rw-r--r--app/views/projects/protected_branches/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/_index.html.haml2
-rw-r--r--changelogs/unreleased/34010-fix-linking-to-parallel-diff-line-number-creating-gray-box.yml4
-rw-r--r--changelogs/unreleased/dm-requirements-txt-tilde.yml5
-rw-r--r--changelogs/unreleased/fix-34019.yml4
-rw-r--r--changelogs/unreleased/issue-inline-edit-quick-submit.yml4
-rw-r--r--changelogs/unreleased/mk-fix-breadcrumb-order-33938.yml4
-rw-r--r--changelogs/unreleased/mk-fix-issue-34068.yml4
-rw-r--r--config/boot.rb12
-rw-r--r--config/initializers/flipper.rb4
-rw-r--r--config/webpack.config.js23
-rw-r--r--doc/ci/quick_start/README.md6
-rw-r--r--doc/user/project/pipelines/settings.md2
-rw-r--r--lib/feature.rb2
-rw-r--r--lib/gitlab/dependency_linker/requirements_txt_linker.rb2
-rw-r--r--lib/gitlab/git/diff.rb2
-rw-r--r--lib/gitlab/gitaly_client/diff_stitcher.rb5
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/helpers/groups_helper_spec.rb13
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb13
-rw-r--r--spec/javascripts/groups/groups_spec.js24
-rw-r--r--spec/javascripts/issue_show/components/fields/description_spec.js20
-rw-r--r--spec/javascripts/issue_show/components/fields/title_spec.js20
-rw-r--r--spec/javascripts/issue_show/helpers.js10
-rw-r--r--spec/javascripts/merge_request_notes_spec.js2
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js138
-rw-r--r--spec/lib/gitlab/dependency_linker/requirements_txt_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb8
51 files changed, 379 insertions, 126 deletions
diff --git a/Gemfile b/Gemfile
index c78c1f2fc4c..4cef03bb40b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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