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:
authorJarka Kadlecova <jarka@gitlab.com>2017-11-03 12:17:28 +0300
committerJarka Kadlecova <jarka@gitlab.com>2017-11-03 12:17:28 +0300
commita0237ede184c15be951f0dbcce89d04a34b5cab4 (patch)
treeeb1f0fce29821e9010fe98d77d765d31cdde5d90
parent57814866a262fd5201e6e8eff710440dd09a1029 (diff)
parent1c15f23b0ddc6599f03dcc63f38b2dc887a902d6 (diff)
Merge branch '10-0-stable-patch-5' into '10-0-stable'
Prepare 10.0.5 release See merge request gitlab-org/gitlab-ce!14935
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/boards/services/board_service.js4
-rw-r--r--app/assets/javascripts/dropzone_input.js5
-rw-r--r--app/assets/javascripts/gl_dropdown.js1
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue18
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue (renamed from app/assets/javascripts/monitoring/components/graph_path.vue)0
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js2
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js24
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/new_nav.scss2
-rw-r--r--app/assets/stylesheets/pages/environments.scss10
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/models/application_setting.rb5
-rw-r--r--app/models/project.rb20
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/views/groups/milestones/_header_title.html.haml3
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--changelogs/unreleased/35580-cannot-import-project-with-milestones.yml5
-rw-r--r--changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml5
-rw-r--r--changelogs/unreleased/37229-mr-widget-status-icon.yml5
-rw-r--r--changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml5
-rw-r--r--changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml5
-rw-r--r--changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml5
-rw-r--r--changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml6
-rw-r--r--changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml5
-rw-r--r--changelogs/unreleased/bvl-dont-rename-free-names.yml5
-rw-r--r--changelogs/unreleased/fix-application-setting-nil-cache.yml5
-rw-r--r--changelogs/unreleased/fix-import-issue-assignees.yml5
-rw-r--r--changelogs/unreleased/fix_global_board_routes_39073.yml5
-rw-r--r--changelogs/unreleased/group-milestones-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/mr-widget-merged-date-tooltip.yml5
-rw-r--r--config/routes.rb26
-rw-r--r--db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb11
-rw-r--r--doc/administration/logs.md24
-rw-r--r--lib/gitlab/git/rev_list.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb51
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/system_check/incoming_email/imap_authentication_check.rb45
-rw-r--r--qa/qa/page/admin/menu.rb2
-rw-r--r--qa/qa/specs/config.rb3
-rw-r--r--spec/features/boards/sidebar_spec.rb34
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph_spec.js6
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js40
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js36
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js15
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js71
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/project.group.json188
-rw-r--r--spec/lib/gitlab/import_export/project.json8
-rw-r--r--spec/lib/gitlab/import_export/project.light.json51
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb138
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml3
-rw-r--r--spec/models/application_setting_spec.rb15
-rw-r--r--spec/services/notification_service_spec.rb12
-rw-r--r--spec/services/projects/create_service_spec.rb66
-rw-r--r--spec/services/projects/update_service_spec.rb45
-rw-r--r--spec/support/board_helpers.rb16
-rw-r--r--spec/support/email_helpers.rb14
71 files changed, 894 insertions, 274 deletions
diff --git a/Gemfile b/Gemfile
index 53a4b614048..6638440cdbe 100644
--- a/Gemfile
+++ b/Gemfile
@@ -281,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~>0.7.0.beta14'
+ gem 'prometheus-client-mmap', '~>0.7.0.beta18'
gem 'raindrops', '~> 0.18'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index e70985bb3c9..de704eaac42 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -624,7 +624,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.7.0.beta14)
+ prometheus-client-mmap (0.7.0.beta18)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -1100,7 +1100,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.7.0.beta14)
+ prometheus-client-mmap (~> 0.7.0.beta18)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 38eea38f949..97e80afa3f8 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -7,7 +7,7 @@ class BoardService {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
method: 'GET',
- url: `${gon.relative_url_root}/boards/${boardId}/issues.json`,
+ url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
}
});
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
@@ -16,7 +16,7 @@ class BoardService {
url: `${listsEndpoint}/generate.json`
}
});
- this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
+ this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 975903159be..591dfe58d95 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -238,9 +238,12 @@ window.DropzoneInput = (function() {
};
const insertToTextArea = function(filename, url) {
- return $(child).val(function(index, val) {
+ const $child = $(child);
+ $child.val(function(index, val) {
return val.replace(`{{${filename}}}`, url);
});
+
+ $child.trigger('change');
};
const appendToTextArea = function(url) {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 50d822eba5a..ff218ccad62 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -548,6 +548,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.positionMenuAbove = function() {
var $menu = this.dropdown.find('.dropdown-menu');
+ $menu.addClass('dropdown-open-top');
$menu.css('top', 'initial');
$menu.css('bottom', '100%');
};
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 6b3e341f936..972f4f7adef 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -3,7 +3,7 @@
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
- import GraphPath from './graph_path.vue';
+ import GraphPath from './graph/path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
@@ -65,7 +65,7 @@
},
computed: {
- outterViewBox() {
+ outerViewBox() {
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
@@ -141,17 +141,19 @@
},
renderAxesPaths() {
- this.timeSeries = createTimeSeries(this.graphData.queries[0],
- this.graphWidth,
- this.graphHeight,
- this.graphHeightOffset);
+ this.timeSeries = createTimeSeries(
+ this.graphData.queries[0],
+ this.graphWidth,
+ this.graphHeight,
+ this.graphHeightOffset,
+ );
if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
const axisXScale = d3.time.scale()
- .range([0, this.graphWidth]);
+ .range([0, this.graphWidth - 70]);
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
@@ -211,7 +213,7 @@
class="prometheus-svg-container"
:style="paddingBottomRootSvg">
<svg
- :viewBox="outterViewBox"
+ :viewBox="outerViewBox"
ref="baseSvg">
<g
class="x-axis"
diff --git a/app/assets/javascripts/monitoring/components/graph_path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 043f1bf66bb..043f1bf66bb 100644
--- a/app/assets/javascripts/monitoring/components/graph_path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 7592af5878e..854636e9a89 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) {
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
- value,
+ value: Number(value),
})),
})),
})),
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index fa7ac994058..1a7da84a424 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -272,6 +272,7 @@
v-model="note"
ref="textarea"
slot="textarea"
+ :disabled="isSubmitting"
placeholder="Write a comment or drag your files here..."
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
index 4078aad7f83..b25cc3443ef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -16,9 +16,9 @@ export default {
<div class="media-body">
<mr-widget-author-and-time
actionText="Closed by"
- :author="mr.closedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.closedAt"
+ :author="mr.closedEvent.author"
+ :dateTitle="mr.closedEvent.updatedAt"
+ :dateReadable="mr.closedEvent.formattedUpdatedAt"
/>
<section class="mr-info-list">
<p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index e452260a4d0..74fc52796a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -69,9 +69,9 @@ export default {
<div class="space-children">
<mr-widget-author-and-time
actionText="Merged by"
- :author="mr.mergedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.mergedAt" />
+ :author="mr.mergedEvent.author"
+ :date-title="mr.mergedEvent.updatedAt"
+ :date-readable="mr.mergedEvent.formattedUpdatedAt" />
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index 557b89b2b84..d38c9167f3e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
- mergeButtonClass() {
- const defaultClass = 'btn btn-small btn-success accept-merge-request';
- const failedClass = `${defaultClass} btn-danger`;
- const inActionClass = `${defaultClass} btn-info`;
+ status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
- return failedClass;
+ return 'failed';
} else if (!pipeline) {
- return defaultClass;
+ return 'success';
} else if (isPipelineActive) {
- return inActionClass;
+ return 'pending';
} else if (isPipelineFailed) {
+ return 'failed';
+ }
+
+ return 'success';
+ },
+ mergeButtonClass() {
+ const defaultClass = 'btn btn-small btn-success accept-merge-request';
+ const failedClass = `${defaultClass} btn-danger`;
+ const inActionClass = `${defaultClass} btn-info`;
+
+ if (this.status === 'failed') {
return failedClass;
+ } else if (this.status === 'pending') {
+ return inActionClass;
}
return defaultClass;
},
+ iconClass() {
+ if (this.status === 'failed' || !this.commitMessage.length || !this.isMergeAllowed() || this.mr.preventMerge) {
+ return 'failed';
+ }
+ return 'success';
+ },
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
@@ -208,7 +224,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
- <status-icon status="success" />
+ <status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 29464662578..2f1c0a4c654 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -37,10 +37,8 @@ export default class MergeRequestStore {
}
this.updatedAt = data.updated_at;
- this.mergedAt = MergeRequestStore.getEventDate(data.merge_event);
- this.closedAt = MergeRequestStore.getEventDate(data.closed_event);
- this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
- this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
+ this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
+ this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
@@ -118,6 +116,14 @@ export default class MergeRequestStore {
}
}
+ static getEventObject(event) {
+ return {
+ author: MergeRequestStore.getAuthorObject(event),
+ updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
+ formattedUpdatedAt: MergeRequestStore.getEventDate(event),
+ };
+ }
+
static getAuthorObject(event) {
if (!event) {
return {};
@@ -131,6 +137,14 @@ export default class MergeRequestStore {
};
}
+ static getEventUpdatedAtDate(event) {
+ if (!event) {
+ return '';
+ }
+
+ return event.updated_at;
+ }
+
static getEventDate(event) {
const timeagoInstance = new Timeago();
@@ -138,7 +152,7 @@ export default class MergeRequestStore {
return '';
}
- return timeagoInstance.format(event.updated_at);
+ return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index c0d8e6c328c..b8b8caca048 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -745,6 +745,10 @@
#{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
+ &.dropdown-open-top {
+ margin-bottom: $dropdown-vertical-offset;
+ }
+
li {
display: block;
padding: 0 1px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 3857226cddb..76d87fd81f4 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -323,6 +323,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
* Dropdowns
*/
$dropdown-width: 300px;
+$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 8c5bafac637..24daf993b2e 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -292,7 +292,7 @@ header.navbar-gitlab-new {
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
- margin-top: 4px;
+ margin-top: $dropdown-vertical-offset;
}
.search {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 9362d80d4e6..7c0be8cb988 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -288,8 +288,14 @@
fill: $black;
}
- .tick > text {
- font-size: 12px;
+ .tick {
+ > line {
+ stroke: $gray-darker;
+ }
+
+ > text {
+ font-size: 12px;
+ }
}
.text-metric-title {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 5d7c85b16ef..750a5b4a930 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -362,7 +362,7 @@
.dropdown-menu {
top: initial;
- bottom: 40px;
+ bottom: 100%;
width: 298px;
}
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3568e72e463..2216ac0402d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -194,7 +194,10 @@ class ApplicationSetting < ActiveRecord::Base
ensure_cache_setup
Rails.cache.fetch(CACHE_KEY) do
- ApplicationSetting.last
+ ApplicationSetting.last.tap do |settings|
+ # do not cache nils
+ raise 'missing settings' unless settings
+ end
end
rescue
# Fall back to an uncached value if there are any problems (e.g. redis down)
diff --git a/app/models/project.rb b/app/models/project.rb
index 9bec7963130..7800054f410 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base
# Storage specific hooks
after_initialize :use_hashed_storage
+ after_create :check_repository_absence!
after_create :ensure_storage_path_exists
after_save :ensure_storage_path_exists, if: :namespace_id_changed?
@@ -228,7 +229,7 @@ class Project < ActiveRecord::Base
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
- validate :check_repository_path_availability, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? }
+ validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -1022,7 +1023,9 @@ class Project < ActiveRecord::Base
expires_full_path_cache # we need to clear cache to validate renames correctly
- if gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
+ # Check if repository with same path already exists on disk we can
+ # skip this for the hashed storage because the path does not change
+ if legacy_storage? && repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk')
return false
end
@@ -1576,6 +1579,19 @@ class Project < ActiveRecord::Base
end
end
+ def check_repository_absence!
+ return if skip_disk_validation
+
+ if repository_storage_path.blank? || repository_with_same_path_already_exists?
+ errors.add(:base, 'There is already a repository with that name on disk')
+ throw :abort
+ end
+ end
+
+ def repository_with_same_path_already_exists?
+ gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
+ end
+
# set last_activity_at to the same as created_at
def set_last_activity_at
update_column(:last_activity_at, self.created_at)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e2a80db06a6..b1695f348ee 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -397,7 +397,7 @@ class NotificationService
end
def relabeled_resource_email(target, labels, current_user, method)
- recipients = labels.flat_map { |l| l.subscribers(target.project) }
+ recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
recipients = notifiable_users(
recipients, :subscription,
target: target,
diff --git a/app/views/groups/milestones/_header_title.html.haml b/app/views/groups/milestones/_header_title.html.haml
index d7fabf53587..24eb39b8e2f 100644
--- a/app/views/groups/milestones/_header_title.html.haml
+++ b/app/views/groups/milestones/_header_title.html.haml
@@ -1 +1,2 @@
-- header_title group_title(@group, "Milestones", group_milestones_path(@group))
+- breadcrumb_title @milestone.title
+- add_to_breadcrumbs "Milestones", group_milestones_path(@group)
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index d84a1fd7ee1..82c6439da9c 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -14,7 +14,7 @@
= render "projects/commits/head"
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- - if show_auto_devops_callout?(@project)
+ - if show_auto_devops_callout?(@project) && !show_new_repo?
= render 'shared/auto_devops_callout'
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 1f540bdaf93..dfc0f9be321 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -25,7 +25,7 @@
show_any: "true",
project_id: @project&.try(:id),
labels: labels_filter_path(false),
- namespace_path: @project.try(:namespace).try(:full_path),
+ namespace_path: @namespace_path,
project_path: @project.try(:path) },
":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
%span.dropdown-toggle-text
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 1dfe380db16..4b9af78bc1a 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -7,7 +7,7 @@
= button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do
= icon('caret-down', class: 'toggle-icon')
- %ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
+ %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
diff --git a/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml b/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml
new file mode 100644
index 00000000000..b28105556db
--- /dev/null
+++ b/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the project import with issues and milestones
+merge_request: 14657
+author:
+type: fixed
diff --git a/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml b/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
new file mode 100644
index 00000000000..3364b1d46b3
--- /dev/null
+++ b/changelogs/unreleased/37105-monitoring-graph-axes-labels-are-inaccurate-and-inconsistent.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect X-axis labels in Prometheus graphs
+merge_request: 14258
+author:
+type: fixed
diff --git a/changelogs/unreleased/37229-mr-widget-status-icon.yml b/changelogs/unreleased/37229-mr-widget-status-icon.yml
new file mode 100644
index 00000000000..6d84d1964ca
--- /dev/null
+++ b/changelogs/unreleased/37229-mr-widget-status-icon.yml
@@ -0,0 +1,5 @@
+---
+title: fix merge request widget status icon for failed CI
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml b/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml
new file mode 100644
index 00000000000..c3c38b35fa7
--- /dev/null
+++ b/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed duplicate notifications when added multiple labels on an issue
+merge_request: 14798
+author:
+type: fixed
diff --git a/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
new file mode 100644
index 00000000000..579c247c4c2
--- /dev/null
+++ b/changelogs/unreleased/38187-38315-fix-dropdown-open-top-bottom-spacing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bottom spacing for dropdowns that open upwards
+merge_request: 14535
+author:
+type: fixed
diff --git a/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml b/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml
new file mode 100644
index 00000000000..df562077fb3
--- /dev/null
+++ b/changelogs/unreleased/38197-fix-ImapAuthenticationCheck.yml
@@ -0,0 +1,5 @@
+---
+title: Fix `rake gitlab:incoming_email:check` and make it report the actual error
+merge_request: 14423
+author:
+type: fixed
diff --git a/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml b/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml
new file mode 100644
index 00000000000..768e296fcd7
--- /dev/null
+++ b/changelogs/unreleased/38202-cannot-rename-a-hashed-project.yml
@@ -0,0 +1,6 @@
+---
+title: Does not check if an invariant hashed storage path exists on disk when renaming
+ projects.
+merge_request: 14428
+author:
+type: fixed
diff --git a/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml b/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
new file mode 100644
index 00000000000..bbfe5d49a3e
--- /dev/null
+++ b/changelogs/unreleased/38789-prometheus-graphs-occasionally-have-incorrect-y-scale.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken Y-axis scaling in some Prometheus graphs
+merge_request: 14693
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-dont-rename-free-names.yml b/changelogs/unreleased/bvl-dont-rename-free-names.yml
new file mode 100644
index 00000000000..60a4ec8afbe
--- /dev/null
+++ b/changelogs/unreleased/bvl-dont-rename-free-names.yml
@@ -0,0 +1,5 @@
+---
+title: Don't rename paths that were freed up when upgrading
+merge_request: 15029
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-application-setting-nil-cache.yml b/changelogs/unreleased/fix-application-setting-nil-cache.yml
new file mode 100644
index 00000000000..a5f028e3d69
--- /dev/null
+++ b/changelogs/unreleased/fix-application-setting-nil-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Fix application setting to cache nil object
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-import-issue-assignees.yml b/changelogs/unreleased/fix-import-issue-assignees.yml
new file mode 100644
index 00000000000..063b6afaf08
--- /dev/null
+++ b/changelogs/unreleased/fix-import-issue-assignees.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing Import/Export issue assignees
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix_global_board_routes_39073.yml b/changelogs/unreleased/fix_global_board_routes_39073.yml
new file mode 100644
index 00000000000..cc9ae8592db
--- /dev/null
+++ b/changelogs/unreleased/fix_global_board_routes_39073.yml
@@ -0,0 +1,5 @@
+---
+title: Allow boards as top level route
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/group-milestones-breadcrumb.yml b/changelogs/unreleased/group-milestones-breadcrumb.yml
new file mode 100644
index 00000000000..87085759fda
--- /dev/null
+++ b/changelogs/unreleased/group-milestones-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed milestone breadcrumb links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/mr-widget-merged-date-tooltip.yml b/changelogs/unreleased/mr-widget-merged-date-tooltip.yml
new file mode 100644
index 00000000000..ea22993ff52
--- /dev/null
+++ b/changelogs/unreleased/mr-widget-merged-date-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed merge request widget merged & closed date tooltip text
+merge_request:
+author:
+type: fixed
diff --git a/config/routes.rb b/config/routes.rb
index 5683725c8a2..06e9a13ee46 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -44,6 +44,19 @@ Rails.application.routes.draw do
get 'readiness' => 'health#readiness'
resources :metrics, only: [:index]
mount Peek::Railtie => '/peek'
+
+ # Boards resources shared between group and projects
+ resources :boards, only: [] do
+ resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do
+ collection do
+ post :generate
+ end
+
+ resources :issues, only: [:index, :create, :update]
+ end
+
+ resources :issues, module: :boards, only: [:index, :update]
+ end
end
# Koding route
@@ -74,19 +87,6 @@ Rails.application.routes.draw do
# Notification settings
resources :notification_settings, only: [:create, :update]
- # Boards resources shared between group and projects
- resources :boards do
- resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do
- collection do
- post :generate
- end
-
- resources :issues, only: [:index, :create, :update]
- end
-
- resources :issues, module: :boards, only: [:index, :update]
- end
-
draw :import
draw :uploads
draw :explore
diff --git a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb
index 9441b236c8d..2125cc046e5 100644
--- a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb
+++ b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb
@@ -13,7 +13,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
.well-known
abuse_reports
admin
- all
api
assets
autocomplete
@@ -24,29 +23,20 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
groups
health_check
help
- hooks
import
invites
- issues
jwt
koding
- member
- merge_requests
- new
- notes
notification_settings
oauth
profile
projects
public
- repository
robots.txt
s
search
sent_notifications
- services
snippets
- teams
u
unicorn_test
unsubscribes
@@ -94,7 +84,6 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration
notification_setting
pipeline_quota
projects
- subgroups
].freeze
def up
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 76e071dc673..c9ed2d84ccb 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -18,8 +18,7 @@ other than production, the corresponding logfile is shown here.)
It contains a structured log for Rails controller requests received from
GitLab, thanks to [Lograge](https://github.com/roidrage/lograge/). Note that
-requests from the API [are not yet logged to this
-file](https://gitlab.com/gitlab-org/gitlab-ce/issues/36189).
+requests from the API are logged to a separate file in `api_json.log`.
Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example:
@@ -73,6 +72,27 @@ In this example we can see that server processed an HTTP request with URL
19:34:53 +0200. Also we can see that request was processed by
`Projects::TreeController`.
+## `api_json.log`
+
+Introduced in GitLab 10.0, this file lives in
+`/var/log/gitlab/gitlab-rails/api_json.log` for Omnibus GitLab packages or in
+`/home/git/gitlab/log/api_json.log` for installations from source.
+
+It helps you see requests made directly to the API. For example:
+
+```json
+{"time":"2017-10-10T12:30:11.579Z","severity":"INFO","duration":16.84,"db":1.57,"view":15.27,"status":200,"method":"POST","path":"/api/v4/internal/allowed","params":{"action":"git-upload-pack","changes":"_any","gl_repository":null,"project":"root/foobar.git","protocol":"ssh","env":"{}","key_id":"[FILTERED]","secret_token":"[FILTERED]"},"host":"127.0.0.1","ip":"127.0.0.1","ua":"Ruby"}
+```
+
+This entry above shows an access to an internal endpoint to check whether an
+associated SSH key can download the project in question via a `git fetch` or
+`git clone`. In this example, we see:
+
+1. `method`: The HTTP method used to make the request
+1. `path`: The relative path of the query
+1. `params`: Key-value pairs passed in a query string or HTTP body. Sensitive parameters (e.g. passwords, tokens, etc.) are filtered out.
+1. `ua`: The User-Agent of the requester
+
## `application.log`
This file lives in `/var/log/gitlab/gitlab-rails/application.log` for
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 2b5785a1f08..1383cd15171 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -29,7 +29,7 @@ module Gitlab
output, status = Gitlab::Popen.popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
unless status.zero?
- raise "Got a non-zero exit code while calling out `#{args.join(' ')}`."
+ raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
end
output.split("\n")
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 2171c6c7bbb..373ea7f28b5 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -19,6 +19,7 @@ project_tree:
- milestone:
- events:
- :push_event_payload
+ - :issue_assignees
- snippets:
- :award_emoji
- notes:
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3bc095a99a9..639f4f0c3f0 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -2,7 +2,7 @@ module Gitlab
module ImportExport
class ProjectTreeRestorer
# Relations which cannot have both group_id and project_id at the same time
- RESTRICT_PROJECT_AND_GROUP = %i(milestones).freeze
+ RESTRICT_PROJECT_AND_GROUP = %i(milestone milestones).freeze
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 380b336395d..352d5032af4 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -35,7 +35,7 @@ module Gitlab
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project.id)
+ @relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper
@user = user
@project = project
@@ -56,22 +56,21 @@ module Gitlab
private
def setup_models
- if @relation_name == :notes
- set_note_author
-
- # attachment is deprecated and note uploads are handled by Markdown uploader
- @relation_hash['attachment'] = nil
+ case @relation_name
+ when :merge_request_diff then setup_st_diff_commits
+ when :merge_request_diff_files then setup_diff
+ when :notes then setup_note
+ when :project_label, :project_labels then setup_label
+ when :milestone, :milestones then setup_milestone
+ else
+ @relation_hash['project_id'] = @project.id
end
update_user_references
update_project_references
- handle_group_label if group_label?
reset_tokens!
remove_encrypted_attributes!
-
- set_st_diff_commits if @relation_name == :merge_request_diff
- set_diff if @relation_name == :merge_request_diff_files
end
def update_user_references
@@ -82,6 +81,12 @@ module Gitlab
end
end
+ def setup_note
+ set_note_author
+ # attachment is deprecated and note uploads are handled by Markdown uploader
+ @relation_hash['attachment'] = nil
+ end
+
# Sets the author for a note. If the user importing the project
# has admin access, an actual mapping with new project members
# will be used. Otherwise, a note stating the original author name
@@ -134,11 +139,9 @@ module Gitlab
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
- def group_label?
- @relation_hash['type'] == 'GroupLabel'
- end
+ def setup_label
+ return unless @relation_hash['type'] == 'GroupLabel'
- def handle_group_label
# If there's no group, move the label to a project label
if @relation_hash['group_id']
@relation_hash['project_id'] = nil
@@ -148,6 +151,14 @@ module Gitlab
end
end
+ def setup_milestone
+ if @relation_hash['group_id']
+ @relation_hash['group_id'] = @project.group.id
+ else
+ @relation_hash['project_id'] = @project.id
+ end
+ end
+
def reset_tokens!
return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s)
@@ -196,14 +207,14 @@ module Gitlab
relation_class: relation_class)
end
- def set_st_diff_commits
+ def setup_st_diff_commits
@relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs')
HashUtil.deep_symbolize_array!(@relation_hash['st_diffs'])
HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits'])
end
- def set_diff
+ def setup_diff
@relation_hash['diff'] = @relation_hash.delete('utf8_diff')
end
@@ -248,7 +259,13 @@ module Gitlab
end
def find_or_create_object!
- finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id]
+ finder_attributes = if @relation_name == :group_label
+ %w[title group_id]
+ elsif parsed_relation_hash['project_id']
+ %w[title project_id]
+ else
+ %w[title group_id]
+ end
finder_hash = parsed_relation_hash.slice(*finder_attributes)
if label?
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 7c02c9c5c48..894bd5efae5 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -26,7 +26,6 @@ module Gitlab
apple-touch-icon.png
assets
autocomplete
- boards
ci
dashboard
deploy.html
diff --git a/lib/system_check/incoming_email/imap_authentication_check.rb b/lib/system_check/incoming_email/imap_authentication_check.rb
index dee108d987b..e55bea86d3f 100644
--- a/lib/system_check/incoming_email/imap_authentication_check.rb
+++ b/lib/system_check/incoming_email/imap_authentication_check.rb
@@ -4,22 +4,17 @@ module SystemCheck
set_name 'IMAP server credentials are correct?'
def check?
- if mailbox_config
- begin
- imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
- imap.starttls if config[:start_tls]
- imap.login(config[:email], config[:password])
- connected = true
- rescue
- connected = false
- end
+ if config
+ try_connect_imap
+ else
+ @error = "#{mail_room_config_path} does not have mailboxes setup"
+ false
end
-
- connected
end
def show_error
try_fixing_it(
+ "An error occurred: #{@error.class}: #{@error.message}",
'Check that the information in config/gitlab.yml is correct'
)
for_more_information(
@@ -30,15 +25,31 @@ module SystemCheck
private
- def mailbox_config
- return @config if @config
+ def try_connect_imap
+ imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
+ imap.starttls if config[:start_tls]
+ imap.login(config[:email], config[:password])
+ true
+ rescue => error
+ @error = error
+ false
+ end
+
+ def config
+ @config ||= load_config
+ end
+
+ def mail_room_config_path
+ @mail_room_config_path ||=
+ Rails.root.join('config', 'mail_room.yml').to_s
+ end
- config_path = Rails.root.join('config', 'mail_room.yml').to_s
- erb = ERB.new(File.read(config_path))
- erb.filename = config_path
+ def load_config
+ erb = ERB.new(File.read(mail_room_config_path))
+ erb.filename = mail_room_config_path
config_file = YAML.load(erb.result)
- @config = config_file[:mailboxes]&.first
+ config_file.dig(:mailboxes, 0)
end
end
end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index f4619042e34..baa06b1c75e 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -4,8 +4,6 @@ module QA
class Menu < Page::Base
def go_to_license
link = find_link 'License'
- # Click space to scroll this link into the view
- link.send_keys(:space)
link.click
end
end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
index 4dfdd6cd93c..79c681168cc 100644
--- a/qa/qa/specs/config.rb
+++ b/qa/qa/specs/config.rb
@@ -43,8 +43,7 @@ module QA
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => {
- 'binary' => '/usr/bin/google-chrome-stable',
- 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024]
+ 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
}
)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c3bf50ef9d1..4965f803883 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
-describe 'Issue Boards', js: true do
+describe 'Issue Boards', :js do
+ include BoardHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, :public) }
@@ -309,6 +311,21 @@ describe 'Issue Boards', js: true do
expect(card).to have_selector('.label', count: 1)
expect(card).not_to have_content(stretch.title)
end
+
+ it 'creates new label' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+ click_link 'Create new label'
+ fill_in 'new_label_name', with: 'test label'
+ first('.suggest-colors-dropdown a').click
+ click_button 'Create'
+ wait_for_requests
+
+ expect(page).to have_link 'test label'
+ end
+ end
end
context 'subscription' do
@@ -322,19 +339,4 @@ describe 'Issue Boards', js: true do
end
end
end
-
- def click_card(card)
- page.within(card) do
- first('.card-number').click
- end
-
- wait_for_sidebar
- end
-
- def wait_for_sidebar
- # loop until the CSS transition is complete
- Timeout.timeout(0.5) do
- loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
- end
- end
end
diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
index a4844636d09..81825a3ae87 100644
--- a/spec/javascripts/monitoring/graph_path_spec.js
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import GraphPath from '~/monitoring/components/graph_path.vue';
+import GraphPath from '~/monitoring/components/graph/path.vue';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data';
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 7d8b0744af1..2b41635584a 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -44,7 +44,7 @@ describe('Graph', () => {
.not.toEqual(-1);
});
- it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
+ it('outerViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({
graphData: convertedMetrics[1],
classType: 'col-md-6',
@@ -52,8 +52,8 @@ describe('Graph', () => {
deploymentData,
});
- const viewBoxArray = component.outterViewBox.split(' ');
- expect(typeof component.outterViewBox).toEqual('string');
+ const viewBoxArray = component.outerViewBox.split(' ');
+ expect(typeof component.outerViewBox).toEqual('string');
expect(viewBoxArray[2]).toEqual(component.graphWidth.toString());
expect(viewBoxArray[3]).toEqual(component.graphHeight.toString());
});
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
index 1c8b1b98242..3f659af5c3b 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -33,6 +33,30 @@ describe('issue_comment_form component', () => {
expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
});
+ describe('handleSave', () => {
+ it('should request to save note when note is entered', () => {
+ vm.note = 'hello world';
+ spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
+ spyOn(vm, 'resizeTextarea');
+ spyOn(vm, 'stopPolling');
+
+ vm.handleSave();
+ expect(vm.isSubmitting).toEqual(true);
+ expect(vm.note).toEqual('');
+ expect(vm.saveNote).toHaveBeenCalled();
+ expect(vm.stopPolling).toHaveBeenCalled();
+ expect(vm.resizeTextarea).toHaveBeenCalled();
+ });
+
+ it('should toggle issue state when no note', () => {
+ spyOn(vm, 'toggleIssueState');
+
+ vm.handleSave();
+
+ expect(vm.toggleIssueState).toHaveBeenCalled();
+ });
+ });
+
describe('textarea', () => {
it('should render textarea with placeholder', () => {
expect(
@@ -40,6 +64,22 @@ describe('issue_comment_form component', () => {
).toEqual('Write a comment or drag your files here...');
});
+ it('should make textarea disabled while requesting', (done) => {
+ const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button'));
+ vm.note = 'hello world';
+ spyOn(vm, 'stopPolling');
+ spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
+
+ vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton.
+ $submitButton.trigger('click');
+
+ vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea.
+ expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy();
+ done();
+ });
+ });
+ });
+
it('should support quick actions', () => {
expect(
vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'),
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
index 47303d1e80f..d23b558f4ea 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -4,11 +4,15 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid
const mr = {
targetBranch: 'good-branch',
targetBranchPath: '/good-branch',
- closedBy: {
- name: 'Fatih Acet',
- username: 'fatihacet',
+ closedEvent: {
+ author: {
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ },
+ updatedAt: 'closedEventUpdatedAt',
+ formattedUpdatedAt: '',
},
- updatedAt: '2017-03-23T20:08:08.845Z',
+ updatedAt: 'mrUpdatedAt',
closedAt: '1 day ago',
};
@@ -18,7 +22,7 @@ const createComponent = () => {
return new Component({
el: document.createElement('div'),
propsData: { mr },
- }).$el;
+ });
};
describe('MRWidgetClosed', () => {
@@ -38,14 +42,30 @@ describe('MRWidgetClosed', () => {
});
describe('template', () => {
- it('should have correct elements', () => {
- const el = createComponent();
+ let vm;
+ let el;
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should have correct elements', () => {
expect(el.querySelector('h4').textContent).toContain('Closed by');
- expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name);
+ expect(el.querySelector('h4').textContent).toContain(mr.closedEvent.author.name);
expect(el.textContent).toContain('The changes were not merged into');
expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
});
+
+ it('should use closedEvent updatedAt as tooltip title', () => {
+ expect(
+ el.querySelector('time').getAttribute('title'),
+ ).toBe('closedEventUpdatedAt');
+ });
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index afaa750199a..2714e8294fa 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -14,9 +14,12 @@ const createComponent = () => {
canRevertInCurrentMR: true,
canRemoveSourceBranch: true,
sourceBranchRemoved: true,
- mergedBy: {},
- mergedAt: '',
- updatedAt: '',
+ mergedEvent: {
+ author: {},
+ updatedAt: 'mergedUpdatedAt',
+ formattedUpdatedAt: '',
+ },
+ updatedAt: 'mrUpdatedAt',
targetBranch,
};
@@ -170,5 +173,11 @@ describe('MRWidgetMerged', () => {
done();
});
});
+
+ it('should use mergedEvent updatedAt as tooltip title', () => {
+ expect(
+ el.querySelector('time').getAttribute('title'),
+ ).toBe('mergedUpdatedAt');
+ });
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 8f69458ffb0..560ed47a4b3 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -95,35 +95,84 @@ describe('MRWidgetReadyToMerge', () => {
});
});
+ describe('status', () => {
+ it('defaults to success', () => {
+ vm.mr.pipeline = true;
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns failed when MR has CI but also has an unknown status', () => {
+ vm.mr.hasCI = true;
+ expect(vm.status).toEqual('failed');
+ });
+
+ it('returns default when MR has no pipeline', () => {
+ expect(vm.status).toEqual('success');
+ });
+
+ it('returns pending when pipeline is active', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+ expect(vm.status).toEqual('pending');
+ });
+
+ it('returns failed when pipeline is failed', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineFailed = true;
+ expect(vm.status).toEqual('failed');
+ });
+ });
+
describe('mergeButtonClass', () => {
const defaultClass = 'btn btn-small btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
- it('should return default class', () => {
+ it('defaults to success class', () => {
+ expect(vm.mergeButtonClass).toEqual(defaultClass);
+ });
+
+ it('returns success class for success status', () => {
vm.mr.pipeline = true;
expect(vm.mergeButtonClass).toEqual(defaultClass);
});
- it('should return failed class when MR has CI but also has an unknown status', () => {
+ it('returns info class for pending status', () => {
+ vm.mr.pipeline = {};
+ vm.mr.isPipelineActive = true;
+ expect(vm.mergeButtonClass).toEqual(inActionClass);
+ });
+
+ it('returns failed class for failed status', () => {
vm.mr.hasCI = true;
expect(vm.mergeButtonClass).toEqual(failedClass);
});
+ });
- it('should return default class when MR has no pipeline', () => {
- expect(vm.mergeButtonClass).toEqual(defaultClass);
+ describe('status icon', () => {
+ it('defaults to tick icon', () => {
+ expect(vm.iconClass).toEqual('success');
});
- it('should return in action class when pipeline is active', () => {
+ it('shows tick for success status', () => {
+ vm.mr.pipeline = true;
+ expect(vm.iconClass).toEqual('success');
+ });
+
+ it('shows tick for pending status', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineActive = true;
- expect(vm.mergeButtonClass).toEqual(inActionClass);
+ expect(vm.iconClass).toEqual('success');
});
- it('should return failed class when pipeline is failed', () => {
- vm.mr.pipeline = {};
- vm.mr.isPipelineFailed = true;
- expect(vm.mergeButtonClass).toEqual(failedClass);
+ it('shows x for failed status', () => {
+ vm.mr.hasCI = true;
+ expect(vm.iconClass).toEqual('failed');
+ });
+
+ it('shows x for merge not allowed', () => {
+ vm.mr.hasCI = true;
+ expect(vm.iconClass).toEqual('failed');
});
});
@@ -177,7 +226,7 @@ describe('MRWidgetReadyToMerge', () => {
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
- it('should return true when there vm instance is making request', () => {
+ it('should return true when the vm instance is making request', () => {
vm.isMakingRequest = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index d57ffcae8e1..492659a82b0 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -21,7 +21,7 @@ describe Gitlab::CurrentSettings do
it 'falls back to DB if Redis returns an empty value' do
expect(ApplicationSetting).to receive(:cached).and_return(nil)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ expect(ApplicationSetting).to receive(:last).and_call_original.twice
expect(current_application_settings).to be_a(ApplicationSetting)
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3fb8edeb701..6ed03c2ca5f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -277,3 +277,6 @@ timelogs:
- user
push_event_payload:
- event
+issue_assignees:
+- issue
+- assignee \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/lib/gitlab/import_export/project.group.json
new file mode 100644
index 00000000000..82a1fbd2fc5
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.group.json
@@ -0,0 +1,188 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "visibility_level": 10,
+ "archived": false,
+ "milestones": [
+ {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ }
+ ],
+ "labels": [
+ {
+ "id": 2,
+ "title": "project label",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "type": "ProjectLabel",
+ "priorities": [
+ {
+ "id": 1,
+ "project_id": 5,
+ "label_id": 1,
+ "priority": 1,
+ "created_at": "2016-10-18T09:35:43.338Z",
+ "updated_at": "2016-10-18T09:35:43.338Z"
+ }
+ ]
+ }
+ ],
+ "issues": [
+ {
+ "id": 1,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 1,
+ "updated_by_id": 1,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ },
+ "label_links": [
+ {
+ "id": 11,
+ "label_id": 6,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "group label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "GroupLabel",
+ "priorities": []
+ }
+ },
+ {
+ "id": 11,
+ "label_id": 2,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "project label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "ProjectLabel",
+ "priorities": []
+ }
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 2,
+ "updated_by_id": 1,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 2,
+ "title": "A group milestone",
+ "description": "Group-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": 100
+ },
+ "label_links": [
+ {
+ "id": 11,
+ "label_id": 2,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 2,
+ "title": "project label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "ProjectLabel",
+ "priorities": []
+ }
+ }
+ ]
+ }
+ ],
+ "snippets": [
+
+ ],
+ "hooks": [
+
+ ]
+}
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 1115fb218d6..9a68bbb379c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -43,7 +43,7 @@
"issues": [
{
"id": 40,
- "title": "Voluptatem amet doloribus deleniti eos maxime repudiandae molestias.",
+ "title": "Voluptatem",
"assignee_id": 1,
"author_id": 22,
"project_id": 5,
@@ -60,6 +60,12 @@
"due_date": null,
"moved_to_id": null,
"test_ee_field": "test",
+ "issue_assignees": [
+ {
+ "user_id": 1,
+ "issue_id": 1
+ }
+ ],
"milestone": {
"id": 1,
"title": "test milestone",
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index 2d8f3d4a566..02450478a77 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -5,9 +5,9 @@
"milestones": [
{
"id": 1,
- "title": "test milestone",
+ "title": "Project milestone",
"project_id": 8,
- "description": "test milestone",
+ "description": "Project-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
@@ -19,7 +19,7 @@
"labels": [
{
"id": 2,
- "title": "test2",
+ "title": "A project label",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
@@ -63,30 +63,21 @@
"last_edited_at": null,
"last_edited_by_id": null,
"group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Project milestone",
+ "project_id": 8,
+ "description": "Project-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ },
"label_links": [
{
"id": 11,
- "label_id": 6,
- "target_id": 1,
- "target_type": "Issue",
- "created_at": "2017-08-15T18:37:40.795Z",
- "updated_at": "2017-08-15T18:37:40.795Z",
- "label": {
- "id": 6,
- "title": "group label",
- "color": "#A8D695",
- "project_id": null,
- "created_at": "2017-08-15T18:37:19.698Z",
- "updated_at": "2017-08-15T18:37:19.698Z",
- "template": false,
- "description": "",
- "group_id": 5,
- "type": "GroupLabel",
- "priorities": []
- }
- },
- {
- "id": 11,
"label_id": 2,
"target_id": 1,
"target_type": "Issue",
@@ -94,14 +85,14 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
- "title": "project label",
+ "title": "Another project label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
"updated_at": "2017-08-15T18:37:19.698Z",
"template": false,
"description": "",
- "group_id": 5,
+ "group_id": null,
"type": "ProjectLabel",
"priorities": []
}
@@ -109,10 +100,6 @@
]
}
],
- "snippets": [
-
- ],
- "hooks": [
-
- ]
+ "snippets": [],
+ "hooks": []
}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index efe11ca794a..76b01b6a1ec 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'JSON' do
it 'restores models based on JSON' do
- expect(@restored_project_json).to be true
+ expect(@restored_project_json).to be_truthy
end
it 'restore correct project features' do
@@ -63,6 +63,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end
+ it 'has issue assignees' do
+ expect(Issue.where(title: 'Voluptatem').first.issue_assignees).not_to be_empty
+ end
+
it 'contains the merge access levels on a protected branch' do
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end
@@ -182,6 +186,53 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
+ shared_examples 'restores project successfully' do
+ it 'correctly restores project' do
+ expect(shared.errors).to be_empty
+ expect(restored_project_json).to be_truthy
+ end
+ end
+
+ shared_examples 'restores project correctly' do |**results|
+ it 'has labels' do
+ expect(project.labels.size).to eq(results.fetch(:labels, 0))
+ end
+
+ it 'has label priorities' do
+ expect(project.labels.first.priorities).not_to be_empty
+ end
+
+ it 'has milestones' do
+ expect(project.milestones.size).to eq(results.fetch(:milestones, 0))
+ end
+
+ it 'has issues' do
+ expect(project.issues.size).to eq(results.fetch(:issues, 0))
+ end
+
+ it 'has issue with group label and project label' do
+ labels = project.issues.first.labels
+
+ expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ end
+ end
+
+ shared_examples 'restores group correctly' do |**results|
+ it 'has group label' do
+ expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
+ end
+
+ it 'has group milestone' do
+ expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
+ end
+
+ it 'has issue with group label' do
+ labels = project.issues.first.labels
+
+ expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ end
+ end
+
context 'Light JSON' do
let(:user) { create(:user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
@@ -190,33 +241,45 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:restored_project_json) { project_tree_restorer.restore }
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
-
allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
end
- context 'project.json file access check' do
- it 'does not read a symlink' do
- Dir.mktmpdir do |tmpdir|
- setup_symlink(tmpdir, 'project.json')
- allow(shared).to receive(:export_path).and_call_original
+ context 'with a simple project' do
+ before do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+
+ restored_project_json
+ end
+
+ it_behaves_like 'restores project correctly',
+ issues: 1,
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
+
+ context 'project.json file access check' do
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'project.json')
+ allow(shared).to receive(:export_path).and_call_original
- restored_project_json
+ restored_project_json
- expect(shared.errors.first).to be_nil
+ expect(shared.errors).to be_empty
+ end
end
end
- end
- context 'when there is an existing build with build token' do
- it 'restores project json correctly' do
- create(:ci_build, token: 'abcd')
+ context 'when there is an existing build with build token' do
+ before do
+ create(:ci_build, token: 'abcd')
+ end
- expect(restored_project_json).to be true
+ it_behaves_like 'restores project successfully'
end
end
- context 'with group' do
+ context 'with a project that has a group' do
let!(:project) do
create(:project,
:builds_disabled,
@@ -227,43 +290,22 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.group.json")
restored_project_json
end
- it 'correctly restores project' do
- expect(restored_project_json).to be_truthy
- expect(shared.errors).to be_empty
- end
-
- it 'has labels' do
- expect(project.labels.count).to eq(2)
- end
-
- it 'creates group label' do
- expect(project.group.labels.count).to eq(1)
- end
-
- it 'has label priorities' do
- expect(project.labels.first.priorities).not_to be_empty
- end
-
- it 'has milestones' do
- expect(project.milestones.count).to eq(1)
- end
-
- it 'has issue' do
- expect(project.issues.count).to eq(1)
- expect(project.issues.first.labels.count).to eq(2)
- end
-
- it 'has issue with group label and project label' do
- labels = project.issues.first.labels
+ it_behaves_like 'restores project successfully'
+ it_behaves_like 'restores project correctly',
+ issues: 2,
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
- expect(labels.where(type: "GroupLabel").count).to eq(1)
- expect(labels.where(type: "ProjectLabel").count).to eq(1)
- end
+ it_behaves_like 'restores group correctly',
+ labels: 1,
+ milestones: 1,
+ first_issue_labels: 1
end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 8e3554375e8..accb6806451 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -77,6 +77,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['issues'].first['notes']).not_to be_empty
end
+ it 'has issue assignees' do
+ expect(saved_project_json['issues'].first['issue_assignees']).not_to be_empty
+ end
+
it 'has author on issue comments' do
expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 899d17d97c2..e06b46aca69 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -474,3 +474,6 @@ ProjectAutoDevops:
- project_id
- created_at
- updated_at
+IssueAssignee:
+- user_id
+- issue_id \ No newline at end of file
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c7a9eabdf06..ee51f2bf13b 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -193,6 +193,21 @@ describe ApplicationSetting do
expect(described_class.current).to eq(:last)
end
end
+
+ context 'when an ApplicationSetting is not yet present' do
+ it 'does not cache nil object' do
+ # when missing settings a nil object is returned, but not cached
+ allow(described_class).to receive(:last).and_return(nil).twice
+ expect(described_class.current).to be_nil
+
+ # when the settings are set the method returns a valid object
+ allow(described_class).to receive(:last).and_return(:last)
+ expect(described_class.current).to eq(:last)
+
+ # subsequent calls get everything from cache
+ expect(described_class.current).to eq(:last)
+ end
+ end
end
context 'restricted signup domains' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 3e493148b32..df8cf9f5aea 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -744,6 +744,18 @@ describe NotificationService, :mailer do
should_not_email(@u_participating)
end
+ it "doesn't send multiple email when a user is subscribed to multiple given labels" do
+ subscriber_to_both = create(:user) do |user|
+ [label_1, label_2].each { |label| label.toggle_subscription(user, project) }
+ end
+
+ notification.relabeled_issue(issue, [label_1, label_2], @u_disabled)
+
+ should_email(subscriber_to_label_1)
+ should_email(subscriber_to_label_2)
+ should_email(subscriber_to_both)
+ end
+
context 'confidential issues' do
let(:author) { create(:user) }
let(:assignee) { create(:user) }
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 7af7e170046..3aed4aa56d1 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -149,6 +149,9 @@ describe Projects::CreateService, '#execute' do
end
context 'when another repository already exists on disk' do
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+
let(:opts) do
{
name: 'Existing',
@@ -156,30 +159,59 @@ describe Projects::CreateService, '#execute' do
}
end
- let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+ context 'with legacy storage' do
+ before do
+ gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ end
- before do
- gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
- end
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ end
- after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
- end
+ it 'does not allow to create a project when path matches existing repository on disk' do
+ project = create_project(user, opts)
- it 'does not allow to create project with same path' do
- project = create_project(user, opts)
+ expect(project).not_to be_persisted
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
- expect(project).to respond_to(:errors)
- expect(project.errors.messages).to have_key(:base)
- expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ it 'does not allow to import project when path matches existing repository on disk' do
+ project = create_project(user, opts.merge({ import_url: 'https://gitlab.com/gitlab-org/gitlab-test.git' }))
+
+ expect(project).not_to be_persisted
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
end
- it 'does not allow to import a project with the same path' do
- project = create_project(user, opts.merge({ import_url: 'https://gitlab.com/gitlab-org/gitlab-test.git' }))
+ context 'with hashed storage' do
+ let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
+ let(:hashed_path) { '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
+
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ allow(Digest::SHA2).to receive(:hexdigest) { hash }
+ end
+
+ before do
+ gitlab_shell.add_repository(repository_storage_path, hashed_path)
+ end
+
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, hashed_path)
+ end
+
+ it 'does not allow to create a project when path matches existing repository on disk' do
+ project = create_project(user, opts)
- expect(project).to respond_to(:errors)
- expect(project.errors.messages).to have_key(:base)
- expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(project).not_to be_persisted
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 5920e79852b..34cd3366e79 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -149,24 +149,43 @@ describe Projects::UpdateService, '#execute' do
end
context 'when renaming a project' do
- let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
- before do
- gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
- end
+ context 'with legacy storage' do
+ before do
+ gitlab_shell.add_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ end
- after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ end
+
+ it 'does not allow renaming when new path matches existing repository on disk' do
+ result = update_project(project, admin, path: 'existing')
+
+ expect(result).to include(status: :error)
+ expect(result[:message]).to match('There is already a repository with that name on disk')
+ expect(project).not_to be_valid
+ expect(project.errors.messages).to have_key(:base)
+ expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
+ end
end
- it 'does not allow renaming when new path matches existing repository on disk' do
- result = update_project(project, admin, path: 'existing')
+ context 'with hashed storage' do
+ let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
- expect(result).to include(status: :error)
- expect(result[:message]).to match('There is already a repository with that name on disk')
- expect(project).not_to be_valid
- expect(project.errors.messages).to have_key(:base)
- expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ end
+
+ it 'does not check if new path matches existing repository on disk' do
+ expect(project).not_to receive(:repository_with_same_path_already_exists?)
+
+ result = update_project(project, admin, path: 'existing')
+
+ expect(result).to include(status: :success)
+ end
end
end
diff --git a/spec/support/board_helpers.rb b/spec/support/board_helpers.rb
new file mode 100644
index 00000000000..507d0432d7f
--- /dev/null
+++ b/spec/support/board_helpers.rb
@@ -0,0 +1,16 @@
+module BoardHelpers
+ def click_card(card)
+ within card do
+ first('.card-number').click
+ end
+
+ wait_for_sidebar
+ end
+
+ def wait_for_sidebar
+ # loop until the CSS transition is complete
+ Timeout.timeout(0.5) do
+ loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
+ end
+ end
+end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index 3e979f2f470..b39052923dd 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,6 +1,6 @@
module EmailHelpers
- def sent_to_user?(user, recipients = email_recipients)
- recipients.include?(user.notification_email)
+ def sent_to_user(user, recipients: email_recipients)
+ recipients.count { |to| to == user.notification_email }
end
def reset_delivered_emails!
@@ -10,17 +10,17 @@ module EmailHelpers
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)
- users.each { |user| should_email(user, recipients) }
+ users.each { |user| should_email(user, recipients: recipients) }
expect(recipients.count).to eq(users.count)
end
- def should_email(user, recipients = email_recipients)
- expect(sent_to_user?(user, recipients)).to be_truthy
+ def should_email(user, times: 1, recipients: email_recipients)
+ expect(sent_to_user(user, recipients: recipients)).to eq(times)
end
- def should_not_email(user, recipients = email_recipients)
- expect(sent_to_user?(user, recipients)).to be_falsey
+ def should_not_email(user, recipients: email_recipients)
+ should_email(user, times: 0, recipients: recipients)
end
def should_not_email_anyone