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:
authorRobert Speicher <rspeicher@gmail.com>2019-01-08 05:03:00 +0300
committerRobert Speicher <rspeicher@gmail.com>2019-01-08 05:03:00 +0300
commitd0bb16fcdc38c6491ff48daf139ae41e55b355ca (patch)
tree66679c94edf0e98eda497305fadc49d404d89548
parentf2fee7bc6b363774fb1c8e1b154b529db2dff02c (diff)
parent710f2ec50c49d1e773acc20058ed584f1402de33 (diff)
Merge branch 'master' into 11-7-stable
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js24
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue12
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue5
-rw-r--r--app/assets/stylesheets/framework/modal.scss38
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/issues.scss8
-rw-r--r--app/controllers/concerns/uploads_actions.rb6
-rw-r--r--app/controllers/projects/issues_controller.rb18
-rw-r--r--app/controllers/projects/settings/operations_controller.rb9
-rw-r--r--app/helpers/projects_helper.rb3
-rw-r--r--app/mailers/emails/issues.rb11
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb14
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project.rb29
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/issues/import_csv_service.rb53
-rw-r--r--app/services/notes/quick_actions_service.rb2
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb14
-rw-r--r--app/services/projects/operations/update_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb41
-rw-r--r--app/services/upload_service.rb2
-rw-r--r--app/validators/url_validator.rb4
-rw-r--r--app/views/notify/import_issues_csv_email.html.haml18
-rw-r--r--app/views/notify/import_issues_csv_email.text.erb11
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/issues/_import_export.svg1
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml41
-rw-r--r--app/views/projects/issues/import_csv/_button.html.haml9
-rw-r--r--app/views/projects/issues/import_csv/_modal.html.haml24
-rw-r--r--app/views/projects/issues/index.html.haml5
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml30
-rw-r--r--app/views/projects/settings/operations/show.html.haml1
-rw-r--r--app/views/shared/empty_states/_issues.html.haml13
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml4
-rw-r--r--app/views/shared/notes/_comment_button.html.haml19
-rw-r--r--app/views/shared/notes/_edit.html.haml2
-rw-r--r--app/views/shared/notes/_edit_form.html.haml8
-rw-r--r--app/views/shared/notes/_form.html.haml6
-rw-r--r--app/views/shared/notes/_hints.html.haml12
-rw-r--r--app/views/shared/notes/_note.html.haml4
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml14
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/import_issues_csv_worker.rb20
-rw-r--r--changelogs/unreleased/49231-import-issues-csv.yml5
-rw-r--r--changelogs/unreleased/53966-hashed-storage-read-only.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-shared-notes.yml5
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20181212171634_create_error_tracking_settings.rb17
-rw-r--r--db/schema.rb8
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md2
-rw-r--r--doc/administration/incoming_email.md70
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/development/emails.md30
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md2
-rw-r--r--doc/university/glossary/README.md12
-rw-r--r--doc/university/high-availability/aws/README.md16
-rw-r--r--doc/user/project/issues/create_new_issue.md40
-rw-r--r--doc/user/project/issues/csv_import.md45
-rw-r--r--doc/user/project/issues/img/import_csv_button.pngbin0 -> 4342 bytes
-rw-r--r--doc/user/project/issues/index.md9
-rw-r--r--doc/user/project/merge_requests/img/create_from_email.pngbin55777 -> 112256 bytes
-rw-r--r--doc/user/project/merge_requests/index.md12
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/gitlab/email/attachment_uploader.rb4
-rw-r--r--lib/gitlab/import_export/import_export.yml4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb1
-rw-r--r--lib/gitlab/import_export/uploads_manager.rb2
-rw-r--r--lib/gitlab/url_blocker.rb18
-rw-r--r--locale/gitlab.pot122
-rw-r--r--package.json2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb50
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb66
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb168
-rw-r--r--spec/db/schema_spec.rb7
-rw-r--r--spec/factories/project_error_tracking_settings.rb10
-rw-r--r--spec/features/projects/settings/operations_settings_spec.rb39
-rw-r--r--spec/fixtures/csv_comma.csv4
-rw-r--r--spec/fixtures/csv_semicolon.csv5
-rw-r--r--spec/fixtures/csv_tab.csv4
-rw-r--r--spec/fixtures/security-reports/master/gl-container-scanning-report.json94
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js36
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb32
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml5
-rw-r--r--spec/mailers/emails/issues_spec.rb33
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb36
-rw-r--r--spec/models/note_spec.rb15
-rw-r--r--spec/models/project_spec.rb46
-rw-r--r--spec/services/issues/import_csv_service_spec.rb64
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb14
-rw-r--r--spec/services/projects/operations/update_service_spec.rb61
-rw-r--r--spec/services/upload_service_spec.rb4
-rw-r--r--spec/validators/url_validator_spec.rb51
-rw-r--r--spec/views/projects/settings/operations/show.html.haml_spec.rb39
-rw-r--r--spec/workers/import_issues_csv_worker_spec.rb21
-rw-r--r--yarn.lock8
98 files changed, 1655 insertions, 248 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 8178821be3d..570d3b712e0 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -221,13 +221,13 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
+ tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
data: GfmAutoComplete.defaultLoadingData,
- // eslint-disable-next-line no-template-curly-in-string
- insertTpl: '${atwho-at}${id}',
+ insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
+ skipSpecialCharacterTest: true,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(issues) {
@@ -238,6 +238,7 @@ class GfmAutoComplete {
return {
id: i.iid,
title: sanitize(i.title),
+ reference: i.reference,
search: `${i.iid} ${i.title}`,
};
});
@@ -287,13 +288,13 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
+ tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
data: GfmAutoComplete.defaultLoadingData,
- // eslint-disable-next-line no-template-curly-in-string
- insertTpl: '${atwho-at}${id}',
+ insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
+ skipSpecialCharacterTest: true,
callbacks: {
...this.getDefaultCallbacks(),
beforeSave(merges) {
@@ -304,6 +305,7 @@ class GfmAutoComplete {
return {
id: m.iid,
title: sanitize(m.title),
+ reference: m.reference,
search: `${m.iid} ${m.title}`,
};
});
@@ -397,7 +399,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
- tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
+ tmpl = GfmAutoComplete.Issues.templateFunction(value);
}
return tmpl;
},
@@ -596,8 +598,12 @@ GfmAutoComplete.Labels = {
};
// Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = {
- templateFunction(id, title) {
- return `<li><small>${id}</small> ${_.escape(title)}</li>`;
+ insertTemplateFunction(value) {
+ // eslint-disable-next-line no-template-curly-in-string
+ return value.reference || '${atwho-at}${id}';
+ },
+ templateFunction({ id, title, reference }) {
+ return `<li><small>${reference || id}</small> ${_.escape(title)}</li>`;
},
};
// Milestones
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 12224e36ba2..e2cffe0b4b4 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -6,6 +6,7 @@ export default {
components: {
GlAreaChart,
},
+ inheritAttrs: false,
props: {
graphData: {
type: Object,
@@ -25,6 +26,11 @@ export default {
);
},
},
+ alertData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
chartData() {
@@ -74,9 +80,6 @@ export default {
const [date, value] = params;
return [dateFormat(date, 'dd mmm yyyy, h:MMtt'), value.toFixed(3)];
},
- onCreated(chart) {
- this.$emit('created', chart);
- },
},
};
</script>
@@ -88,10 +91,11 @@ export default {
<div class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
+ v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
- @created="onCreated"
+ :thresholds="alertData"
/>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2d9c5050c9b..cea5c1a56ca 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -144,6 +144,9 @@ export default {
}
},
methods: {
+ getGraphAlerts(graphId) {
+ return this.alertData ? this.alertData[graphId] || {} : {};
+ },
getGraphsData() {
this.state = 'loading';
Promise.all([
@@ -223,6 +226,8 @@ export default {
:tags-path="tagsPath"
:show-legend="showLegend"
:small-graph="forceSmallGraph"
+ :alert-data="getGraphAlerts(graphData.id)"
+ group-id="monitor-area-chart"
>
<!-- EE content -->
{{ null }}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 46d40ea7aa5..ace46e32b18 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -101,3 +101,41 @@ body.modal-open {
margin: 0;
}
}
+
+.issues-import-modal,
+.issues-export-modal {
+ .modal-header {
+ justify-content: flex-start;
+
+ .import-export-svg-container {
+ flex-grow: 1;
+ height: 56px;
+ padding: $gl-btn-padding $gl-btn-padding 0;
+
+ > svg {
+ float: right;
+ height: 100%;
+ }
+ }
+ }
+
+ .modal-body {
+ padding: 0;
+
+ .modal-subheader {
+ justify-content: flex-start;
+ align-items: center;
+ border-bottom: 1px solid $modal-border-color;
+ padding: 14px;
+ }
+
+ .modal-text {
+ padding: $gl-padding-24 $gl-padding;
+ min-height: $modal-body-height;
+ }
+ }
+
+ .checkmark {
+ color: $green-400;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d92d81b2cb5..242977e8543 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -656,6 +656,7 @@ $border-color-settings: #e1e1e1;
Modals
*/
$modal-body-height: 134px;
+$modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index bb6b6f84849..6c847fc0d53 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -155,6 +155,14 @@ ul.related-merge-requests > li {
}
}
+.issues-nav-controls {
+ font-size: 0;
+
+ .btn-group:empty {
+ display: none;
+ }
+}
+
.issuable-email-modal-btn {
padding: 0;
color: $blue-600;
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 0eea0cdd50f..c114e16edf8 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -7,12 +7,12 @@ module UploadsActions
UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
def create
- link_to_file = UploadService.new(model, params[:file], uploader_class).execute
+ uploader = UploadService.new(model, params[:file], uploader_class).execute
respond_to do |format|
- if link_to_file
+ if uploader
format.json do
- render json: { link: link_to_file }
+ render json: { link: uploader.to_h }
end
else
format.json do
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 5ed46fc0545..21688e54481 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,7 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
def self.issue_except_actions
- %i[index calendar new create bulk_update]
+ %i[index calendar new create bulk_update import_csv]
end
def self.set_issuables_index_only_actions
@@ -37,6 +37,8 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request_from!, only: [:create_merge_request]
+ before_action :authorize_import_issues!, only: [:import_csv]
+
before_action :set_suggested_issues_feature_flags, only: [:new]
respond_to :html
@@ -175,6 +177,20 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
+ def import_csv
+ return render_404 unless Feature.enabled?(:issues_import_csv)
+
+ if uploader = UploadService.new(project, params[:file]).execute
+ ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id)
+
+ flash[:notice] = _("Your issues are being imported. Once finished, you'll get a confirmation email.")
+ else
+ flash[:alert] = _("File upload error.")
+ end
+
+ redirect_to project_issues_path(project)
+ end
+
protected
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index ae8ac61ad46..521ec2acebb 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -6,6 +6,8 @@ module Projects
before_action :check_license
before_action :authorize_update_environment!
+ helper_method :error_tracking_setting
+
def show
end
@@ -22,13 +24,18 @@ module Projects
private
+ def error_tracking_setting
+ @error_tracking_setting ||= project.error_tracking_setting ||
+ project.build_error_tracking_setting
+ end
+
def update_params
params.require(:project).permit(permitted_project_params)
end
# overridden in EE
def permitted_project_params
- {}
+ { error_tracking_setting_attributes: [:enabled, :api_url, :token] }
end
def check_license
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index af7a262e32c..e67c327f7f8 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -285,7 +285,7 @@ module ProjectsHelper
# overridden in EE
def settings_operations_available?
- false
+ Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project)
end
private
@@ -549,6 +549,7 @@ module ProjectsHelper
services#edit
repository#show
ci_cd#show
+ operations#show
badges#index
pages#show
]
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 370e6d2f90b..654ae211310 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -77,6 +77,17 @@ module Emails
mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason))
end
+ def import_issues_csv_email(user_id, project_id, results)
+ @user = User.find(user_id)
+ @project = Project.find(project_id)
+ @results = results
+
+ mail(to: @user.notification_email, subject: subject('Imported issues')) do |format|
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
+ end
+ end
+
private
def setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 2ac4610967d..80e0a17c312 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -76,6 +76,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.changed_milestone_issue_email(user.id, issue.id, milestone, user.id)
end
+ def import_issues_csv_email
+ Notify.import_issues_csv_email(user, project, { success: 3, errors: [5, 6, 7], valid_file: true })
+ end
+
def closed_merge_request_email
Notify.closed_merge_request_email(user.id, issue.id, user.id).message
end
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
new file mode 100644
index 00000000000..632c64c2f1c
--- /dev/null
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectErrorTrackingSetting < ActiveRecord::Base
+ belongs_to :project
+
+ validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true }
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm'
+ end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index becf14e9785..1578ae9c4cc 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -456,6 +456,10 @@ class Note < ActiveRecord::Base
Upload.find_by(model: self, path: paths)
end
+ def parent
+ project
+ end
+
private
def keep_around_commit
diff --git a/app/models/project.rb b/app/models/project.rb
index e2f010a0432..cab173503ce 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -187,6 +187,7 @@ class Project < ActiveRecord::Base
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :project_repository, inverse_of: :project
+ has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
@@ -295,6 +296,8 @@ class Project < ActiveRecord::Base
allow_destroy: true,
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
+ accepts_nested_attributes_for :error_tracking_setting, update_only: true
+
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
@@ -1786,6 +1789,24 @@ class Project < ActiveRecord::Base
handle_update_attribute_error(e, value)
end
+ # Tries to set repository as read_only, checking for existing Git transfers in progress beforehand
+ #
+ # @return [Boolean] true when set to read_only or false when an existing git transfer is in progress
+ def set_repository_read_only!
+ with_lock do
+ break false if git_transfer_in_progress?
+
+ update_column(:repository_read_only, true)
+ end
+ end
+
+ # Set repository as writable again
+ def set_repository_writable!
+ with_lock do
+ update_column(repository_read_only, false)
+ end
+ end
+
def pushes_since_gc
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
end
@@ -1900,15 +1921,17 @@ class Project < ActiveRecord::Base
def migrate_to_hashed_storage!
return unless storage_upgradable?
- update!(repository_read_only: true)
-
- if repo_reference_count > 0 || wiki_reference_count > 0
+ if git_transfer_in_progress?
ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
else
ProjectMigrateHashedStorageWorker.perform_async(id)
end
end
+ def git_transfer_in_progress?
+ repo_reference_count > 0 || wiki_reference_count > 0
+ end
+
def storage_version=(value)
super
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 3146f26bed5..d70417e710e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -222,6 +222,8 @@ class ProjectPolicy < BasePolicy
rule { owner | admin | guest | group_member }.prevent :request_access
rule { ~request_access_enabled }.prevent :request_access
+ rule { can?(:developer_access) & can?(:create_issue) }.enable :import_issues
+
rule { can?(:developer_access) }.policy do
enable :admin_merge_request
enable :admin_milestone
diff --git a/app/services/issues/import_csv_service.rb b/app/services/issues/import_csv_service.rb
new file mode 100644
index 00000000000..ef08fafa7cc
--- /dev/null
+++ b/app/services/issues/import_csv_service.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Issues
+ class ImportCsvService
+ def initialize(user, project, csv_io)
+ @user = user
+ @project = project
+ @csv_io = csv_io
+ @results = { success: 0, error_lines: [], parse_error: false }
+ end
+
+ def execute
+ process_csv
+ email_results_to_user
+
+ @results
+ end
+
+ private
+
+ def process_csv
+ csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
+
+ CSV.new(csv_data, col_sep: detect_col_sep(csv_data.lines.first), headers: true).each.with_index(2) do |row, line_no|
+ issue = Issues::CreateService.new(@project, @user, title: row[0], description: row[1]).execute
+
+ if issue.persisted?
+ @results[:success] += 1
+ else
+ @results[:error_lines].push(line_no)
+ end
+ end
+ rescue ArgumentError, CSV::MalformedCSVError
+ @results[:parse_error] = true
+ end
+
+ def email_results_to_user
+ Notify.import_issues_csv_email(@user.id, @project.id, @results).deliver_now
+ end
+
+ def detect_col_sep(header)
+ if header.include?(",")
+ ","
+ elsif header.include?(";")
+ ";"
+ elsif header.include?("\t")
+ "\t"
+ else
+ raise CSV::MalformedCSVError
+ end
+ end
+ end
+end
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index 4c14d834949..7ee9732040d 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -31,7 +31,7 @@ module Notes
return if command_params.empty?
return unless supported?(note)
- self.class.noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
+ self.class.noteable_update_service(note).new(note.parent, current_user, command_params).execute(note.noteable)
end
end
end
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index f3e026ba38c..2d851866a18 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -2,6 +2,8 @@
module Projects
module HashedStorage
+ RepositoryMigrationError = Class.new(StandardError)
+
class MigrateRepositoryService < BaseService
include Gitlab::ShellAdapter
@@ -16,6 +18,8 @@ module Projects
end
def execute
+ try_to_set_repository_read_only!
+
@old_storage_version = project.storage_version
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
project.ensure_storage_path_exists
@@ -48,6 +52,16 @@ module Projects
private
+ def try_to_set_repository_read_only!
+ # Mitigate any push operation to start during migration
+ unless project.set_repository_read_only!
+ migration_error = "Target repository '#{old_disk_path}' cannot be made read-only as there is a git transfer in progress"
+ logger.error migration_error
+
+ raise RepositoryMigrationError, migration_error
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def has_wiki?
gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index 7ff0599ee95..abd6d8de750 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -12,7 +12,7 @@ module Projects
private
def project_update_params
- {}
+ params.slice(:error_tracking_setting_attributes)
end
end
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index d248b10f41e..5c58caee8cd 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -2,6 +2,7 @@
module QuickActions
class InterpretService < BaseService
+ include Gitlab::Utils::StrongMemoize
include Gitlab::QuickActions::Dsl
attr_reader :issuable
@@ -210,15 +211,9 @@ module QuickActions
end
params '~label1 ~"label 2"'
condition do
- if project
- available_labels = LabelsFinder
- .new(current_user, project_id: project.id, include_ancestor_groups: true)
- .execute
- end
-
- project &&
- current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
- available_labels.any?
+ parent &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", parent) &&
+ find_labels.any?
end
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
@@ -245,7 +240,7 @@ module QuickActions
issuable.is_a?(Issuable) &&
issuable.persisted? &&
issuable.labels.any? &&
- current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ current_user.can?(:"admin_#{issuable.to_ability_name}", parent)
end
command :unlabel do |labels_param = nil|
if labels_param.present?
@@ -674,9 +669,25 @@ module QuickActions
MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute
end
- def find_labels(labels_param)
- extract_references(labels_param, :label) |
- LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
+ def parent
+ project || group
+ end
+
+ def group
+ strong_memoize(:group) do
+ issuable.group if issuable.respond_to?(:group)
+ end
+ end
+
+ def find_labels(labels_params = nil)
+ finder_params = { include_ancestor_groups: true }
+ finder_params[:project_id] = project.id if project
+ finder_params[:group_id] = group.id if group
+ finder_params[:name] = labels_params.split if labels_params
+
+ result = LabelsFinder.new(current_user, finder_params).execute
+
+ extract_references(labels_params, :label) | result
end
def find_label_references(labels_param)
@@ -707,9 +718,11 @@ module QuickActions
# rubocop: disable CodeReuse/ActiveRecord
def extract_references(arg, type)
+ return [] unless arg
+
ext = Gitlab::ReferenceExtractor.new(project, current_user)
- ext.analyze(arg, author: current_user)
+ ext.analyze(arg, author: current_user, group: group)
ext.references(type)
end
diff --git a/app/services/upload_service.rb b/app/services/upload_service.rb
index 39909ee4f82..41ca95b3b6f 100644
--- a/app/services/upload_service.rb
+++ b/app/services/upload_service.rb
@@ -11,7 +11,7 @@ class UploadService
uploader = @uploader_class.new(@model, nil, @uploader_context)
uploader.store!(@file)
- uploader.to_h
+ uploader
end
private
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
index 5feb0b0f05b..d3e32818dc7 100644
--- a/app/validators/url_validator.rb
+++ b/app/validators/url_validator.rb
@@ -26,6 +26,7 @@
# - allow_local_network: Allow urls pointing to private network addresses. Default: true
# - ports: Allowed ports. Default: all.
# - enforce_user: Validate user format. Default: false
+# - enforce_sanitization: Validate that there are no html/css/js tags. Default: false
#
# Example:
# class User < ActiveRecord::Base
@@ -70,7 +71,8 @@ class UrlValidator < ActiveModel::EachValidator
allow_localhost: true,
allow_local_network: true,
ascii_only: false,
- enforce_user: false
+ enforce_user: false,
+ enforce_sanitization: false
}
end
diff --git a/app/views/notify/import_issues_csv_email.html.haml b/app/views/notify/import_issues_csv_email.html.haml
new file mode 100644
index 00000000000..f30d2b5f078
--- /dev/null
+++ b/app/views/notify/import_issues_csv_email.html.haml
@@ -0,0 +1,18 @@
+- text_style = 'font-size:16px; text-align:center; line-height:30px;'
+
+%p{ style: text_style }
+ Your CSV import for project
+ %a{ href: project_url(@project), style: "color:#3777b0; text-decoration:none;" }
+ = @project.full_name
+ has been completed.
+
+%p{ style: text_style }
+ #{pluralize(@results[:success], 'issue')} imported.
+
+- if @results[:error_lines].present?
+ %p{ style: text_style }
+ Errors found on line #{'number'.pluralize(@results[:error_lines].size)}: #{@results[:error_lines].join(', ')}. Please check if these lines have an issue title.
+
+- if @results[:parse_error]
+ %p{ style: text_style }
+ Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values.
diff --git a/app/views/notify/import_issues_csv_email.text.erb b/app/views/notify/import_issues_csv_email.text.erb
new file mode 100644
index 00000000000..1117f90714d
--- /dev/null
+++ b/app/views/notify/import_issues_csv_email.text.erb
@@ -0,0 +1,11 @@
+Your CSV import for project <%= @project.full_name %> (<%= project_url(@project) %>) has been completed.
+
+<%= pluralize(@results[:success], 'issue') %> imported.
+
+<% if @results[:error_lines].present? %>
+Errors found on line <%= 'number'.pluralize(@results[:error_lines].size) %>: <%= @results[:error_lines].join(', ') %>. Please check if these lines have an issue title.
+<% end %>
+
+<% if @results[:parse_error] %>
+Error parsing CSV file. Please make sure it has the correct format: a delimited text file that uses a comma to separate values.
+<% end %>
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index cecc139b183..888be4ee282 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -24,6 +24,6 @@
= _("No file selected")
= f.file_field :bfg_object_map, accept: 'text/plain', class: "hidden js-object-map-input", required: true
.form-text.text-muted
- = _("The maximum file size allowed is %{max_attachment_size}mb") % { max_attachment_size: Gitlab::CurrentSettings.max_attachment_size }
+ = _("The maximum file size allowed is %{size}.") % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) }
= f.submit _('Start cleanup'), class: 'btn btn-success'
diff --git a/app/views/projects/issues/_import_export.svg b/app/views/projects/issues/_import_export.svg
new file mode 100644
index 00000000000..53c35d12f57
--- /dev/null
+++ b/app/views/projects/issues/_import_export.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 238 111" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="4" width="82" rx="3" height="28" fill="#fff"/><path id="5" d="m68.926 12.09v-2.41c0-.665-.437-.888-.975-.507l-6.552 4.631c-.542.383-.539.998 0 1.379l6.552 4.631c.542.383.975.154.975-.507v-2.41h4.874c.668 0 1.2-.538 1.2-1.201v-2.406c0-.668-.537-1.201-1.2-1.201h-4.874" fill="#fc8a51"/><path id="6" d="m4 24h74v-20h-74v20m-4-21c0-1.655 1.338-2.996 2.991-2.996h76.02c1.652 0 2.991 1.35 2.991 2.996v22.01c0 1.655-1.338 2.996-2.991 2.996h-76.02c-1.652 0-2.991-1.35-2.991-2.996v-22.01"/><circle id="2" cx="16" cy="14" r="7"/><circle id="0" cx="16" cy="14" r="7"/><mask id="3" width="14" height="14" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="14" height="14" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd"><rect width="98" height="111" fill="#fff" rx="6"/><path fill="#e5e5e5" fill-rule="nonzero" d="m4 6.01v98.99c0 1.11.897 2.01 2 2.01h85.998c1.105 0 2-.897 2-2.01v-98.99c0-1.11-.897-2.01-2-2.01h-85.998c-1.105 0-2 .897-2 2.01m-4 0c0-3.318 2.685-6.01 6-6.01h85.998c3.314 0 6 2.689 6 6.01v98.99c0 3.318-2.685 6.01-6 6.01h-85.998c-3.314 0-6-2.689-6-6.01v-98.99"/><rect width="76" height="85" x="11" y="12" fill="#f9f9f9" rx="3"/><g transform="translate(37 59)"><use xlink:href="#4"/><path fill="#e5e5e5" fill-rule="nonzero" d="m4 24h74v-20h-74v20m-4-21c0-1.655 1.338-2.996 2.991-2.996h76.02c1.652 0 2.991 1.35 2.991 2.996v22.01c0 1.655-1.338 2.996-2.991 2.996h-76.02c-1.652 0-2.991-1.35-2.991-2.996v-22.01"/><use fill="#fff" stroke="#6b4fbb" stroke-width="8" mask="url(#1)" xlink:href="#0"/><use xlink:href="#5"/></g><g transform="translate(140)"><path fill="#fff" d="m0 4h94v103h-94z"/><path fill="#e5e5e5" fill-rule="nonzero" d="m0 74v30.993c0 3.318 2.687 6.01 6 6.01h85.998c3.316 0 6-2.69 6-6.01v-98.99c0-3.318-2.687-6.01-6-6.01h-85.998c-3.316 0-6 2.69-6 6.01v.993h4v-.993c0-1.11.896-2.01 2-2.01h85.998c1.105 0 2 .897 2 2.01v98.99c0 1.11-.896 2.01-2 2.01h-85.998c-1.105 0-2-.897-2-2.01v-30.993h-4"/><g fill="#f9f9f9"><rect width="82" height="28" x="8" y="12" rx="3"/><rect width="82" height="28" x="8" y="43" rx="3"/></g></g><g fill-rule="nonzero" transform="translate(148 73)"><use fill="#e5e5e5" xlink:href="#6"/><path fill="#6b4fbb" d="m17 17c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3m0 4c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7"/></g><g transform="translate(25 24)"><use xlink:href="#4"/><use fill="#e5e5e5" fill-rule="nonzero" xlink:href="#6"/><use fill="#fff" stroke="#6b4fbb" stroke-width="8" mask="url(#3)" xlink:href="#2"/><use xlink:href="#5"/></g><g transform="translate(107 10)"><use xlink:href="#4"/><use fill="#fc8a51" fill-opacity=".3" fill-rule="nonzero" xlink:href="#6"/><path fill="#6b4fbb" fill-rule="nonzero" d="m16 17c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3m0 4c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7" id="7"/><use xlink:href="#5"/></g><g transform="translate(128 41)"><use xlink:href="#4"/><use fill="#fc8a51" fill-opacity=".3" fill-rule="nonzero" xlink:href="#6"/><use xlink:href="#7"/><path fill="#fc8a51" d="m66.926 12.09v-2.41c0-.665-.437-.888-.975-.507l-6.552 4.631c-.542.383-.539.998 0 1.379l6.552 4.631c.542.383.975.154.975-.507v-2.41h4.874c.668 0 1.2-.538 1.2-1.201v-2.406c0-.668-.537-1.201-1.2-1.201h-4.874"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index e4a0d4b8479..fd6559e37ba 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,11 +1,30 @@
-= render 'shared/issuable/feed_buttons'
-
-- if @can_bulk_update
- = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
-- if show_new_issue_link?(@project)
- = link_to "New issue", new_project_issue_path(@project,
- issue: { assignee_id: finder.assignee.try(:id),
- milestone_id: finder.milestones.first.try(:id) }),
- class: "btn btn-success",
- title: "New issue",
- id: "new_issue_link"
+- show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true)
+- show_import_button = local_assigns.fetch(:show_import_button, true) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project)
+- show_export_button = local_assigns.fetch(:show_export_button, true)
+
+.nav-controls.issues-nav-controls
+ - if show_feed_buttons
+ = render 'shared/issuable/feed_buttons'
+
+ .btn-group.append-right-10<
+ - if show_export_button
+ = render_if_exists 'projects/issues/export_csv/button'
+
+ - if show_import_button
+ = render 'projects/issues/import_csv/button'
+
+ - if @can_bulk_update
+ = button_tag _("Edit issues"), class: "btn btn-default append-right-10 js-bulk-update-toggle"
+ - if show_new_issue_link?(@project)
+ = link_to _("New issue"), new_project_issue_path(@project,
+ issue: { assignee_id: finder.assignee.try(:id),
+ milestone_id: finder.milestones.first.try(:id) }),
+ class: "btn btn-success",
+ title: _("New issue"),
+ id: "new_issue_link"
+
+- if show_export_button
+ = render_if_exists 'projects/issues/export_csv/modal'
+
+- if show_import_button
+ = render 'projects/issues/import_csv/modal'
diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml
new file mode 100644
index 00000000000..acc2c50294f
--- /dev/null
+++ b/app/views/projects/issues/import_csv/_button.html.haml
@@ -0,0 +1,9 @@
+- type = local_assigns.fetch(:type, :icon)
+
+%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
+ data: { toggle: 'modal', target: '.issues-import-modal' } }
+ - if type == :icon
+ = sprite_icon('upload')
+ - else
+ = _('Import CSV')
+
diff --git a/app/views/projects/issues/import_csv/_modal.html.haml b/app/views/projects/issues/import_csv/_modal.html.haml
new file mode 100644
index 00000000000..18768307341
--- /dev/null
+++ b/app/views/projects/issues/import_csv/_modal.html.haml
@@ -0,0 +1,24 @@
+.issues-import-modal.modal
+ .modal-dialog
+ .modal-content
+ = form_tag import_csv_namespace_project_issues_path, multipart: true do
+ .modal-header
+ %h3
+ = _('Import issues')
+ .import-export-svg-container
+ = render 'projects/issues/import_export.svg'
+ %a.close{ href: '#', 'data-dismiss' => 'modal' } ×
+ .modal-body
+ .modal-text
+ %p
+ = _("Your issues will be imported in the background. Once finished, you'll get a confirmation email.")
+ .form-group
+ = label_tag :file, _('Upload CSV file'), class: 'label-bold'
+ %div
+ = file_field_tag :file, accept: '.csv,text/csv', required: true
+ %p.text-secondary
+ = _('It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected.')
+ = _('The maximum file size allowed is %{size}.') % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) }
+ .modal-footer
+ %button{ type: 'submit', class: 'btn btn-success', title: _('Import issues') }
+ = _('Import issues')
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 1e7737aeb97..39e9e9171cf 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -11,8 +11,7 @@
%div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls
- = render "projects/issues/nav_btns"
+ = render "projects/issues/nav_btns"
= render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update
@@ -23,4 +22,4 @@
- if new_issue_email
= render 'projects/issuable_by_email', email: new_issue_email, issuable_type: 'issue'
- else
- = render 'shared/empty_states/issues', button_path: new_project_issue_path(@project)
+ = render 'shared/empty_states/issues', button_path: new_project_issue_path(@project), show_import_button: true
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
new file mode 100644
index 00000000000..71335e4dfd0
--- /dev/null
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -0,0 +1,30 @@
+- return unless Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project)
+
+- setting = error_tracking_setting
+
+%section.settings.expanded.border-0.no-animate
+ .settings-header
+ %h4
+ = _('Error Tracking')
+ %p
+ = _('To link Sentry to GitLab, enter your Sentry URL and Auth Token.')
+ .settings-content
+ = form_for @project, url: project_settings_operations_path(@project), method: :patch do |f|
+ = form_errors(@project)
+ .form-group
+ = f.fields_for :error_tracking_setting_attributes, setting do |form|
+ .form-check.form-group
+ = form.check_box :enabled, class: 'form-check-input'
+ = form.label :enabled, _('Active'), class: 'form-check-label'
+ .form-group
+ = form.label :api_url, _('Sentry API URL'), class: 'label-bold'
+ = form.url_field :api_url, class: 'form-control', placeholder: _('http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/issues/')
+ %p.form-text.text-muted
+ = _('Enter your Sentry API URL')
+ .form-group
+ = form.label :token, _('Auth Token'), class: 'label-bold'
+ = form.text_field :token, class: 'form-control'
+ %p.form-text.text-muted
+ = _('Find and manage Auth Tokens in your Sentry account settings page.')
+
+ = f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 0782029dbcd..b36fa9a5f51 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -1,4 +1,5 @@
- @content_class = 'limit-container-width' unless fluid_layout
- page_title _('Operations')
+= render 'projects/settings/operations/error_tracking', expanded: true
= render_if_exists 'projects/settings/operations/tracing'
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 0ddc56dc6c3..0434860dec4 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -1,5 +1,6 @@
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
+- show_import_button = local_assigns.fetch(:show_import_button, false) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project)
- has_button = button_path || project_select_button
.row.empty-state
@@ -21,12 +22,20 @@
- if has_button
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues, with_feature_enabled: 'issues'
+ = render 'shared/new_project_item_select', path: 'issues/new', label: _('New issue'), type: :issues, with_feature_enabled: 'issues'
- else
- = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
+ = link_to _('New issue'), button_path, class: 'btn btn-success', title: _('New issue'), id: 'new_issue_link'
+
+ - if show_import_button
+ = render 'projects/issues/import_csv/button', type: :text
+
- else
%h4.text-center= _("There are no issues to show")
%p
= _("The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.")
.text-center
= link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success'
+
+- if show_import_button
+ = render 'projects/issues/import_csv/modal'
+
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
index d4834090413..83f60fa6fe2 100644
--- a/app/views/shared/issuable/_feed_buttons.html.haml
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -1,4 +1,4 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to RSS feed' do
+= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
= icon('rss')
-= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to calendar' do
+= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
= custom_icon('icon_calendar')
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 0674c822d63..f487c0bf0d5 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,30 +1,31 @@
- noteable_name = @note.noteable.human_class_name
.float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
- %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
+ %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment') }
- if @note.can_be_discussion_note?
- = 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
+ = 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.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}" } }
+ %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => _('Comment'), 'close-text' => _("Comment & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Comment & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
- %strong Comment
+ %strong= _("Comment")
%p
- Add a general comment to this #{noteable_name}.
+ = _("Add a general comment to this %{noteable_name}.") % { noteable_name: noteable_name }
%li.divider.droplab-item-ignore
- %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
+ %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => _('Start discussion'), 'close-text' => _("Start discussion & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Start discussion & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
- %strong Start discussion
+ %strong= _("Start discussion")
%p
= succeed '.' do
- Discuss a specific suggestion or question
- if @note.noteable.supports_resolvable_notes?
- that needs to be resolved
+ = _('Discuss a specific suggestion or question that needs to be resolved')
+ - else
+ = _('Discuss a specific suggestion or question')
diff --git a/app/views/shared/notes/_edit.html.haml b/app/views/shared/notes/_edit.html.haml
index f4b3aac29b4..84a3ef9d8fe 100644
--- a/app/views/shared/notes/_edit.html.haml
+++ b/app/views/shared/notes/_edit.html.haml
@@ -1 +1 @@
-%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note
+%textarea.hidden.js-task-list-field.original-task-list{ data: { update_url: note_url(note) } }= note.note
diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml
index fec966069b9..6fe511c2999 100644
--- a/app/views/shared/notes/_edit_form.html.haml
+++ b/app/views/shared/notes/_edit_form.html.haml
@@ -3,12 +3,12 @@
= hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
- = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here…"
+ = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: _("Write a comment or drag your files here…")
= render 'shared/notes/hints'
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-finish-edit-warning
- Finish editing this message first!
- = submit_tag 'Save comment', class: 'btn btn-nr btn-success js-comment-save-button'
+ = _("Finish editing this message first!")
+ = submit_tag _('Save comment'), class: 'btn btn-nr btn-success js-comment-save-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
- Cancel
+ = _("Cancel")
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 493c6241257..6a1eea85fde 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -29,7 +29,7 @@
= render 'projects/zen', f: f,
attr: :note,
classes: 'note-textarea js-note-text',
- placeholder: "Write a comment or drag your files here…",
+ placeholder: _("Write a comment or drag your files here…"),
supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
@@ -40,5 +40,5 @@
= yield(:note_actions)
- %a.btn.btn-cancel.js-close-discussion-note-form.hide{ role: "button", data: {cancel_text: "Cancel" } }
- Cancel
+ %a.btn.btn-cancel.js-close-discussion-note-form.hide{ role: "button", data: { cancel_text: _("Cancel") } }
+ = _('Cancel')
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 00eae553279..46f3f8428f1 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -1,10 +1,10 @@
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
.comment-toolbar.clearfix
.toolbar-text
- = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+ = link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank', tabindex: -1
- if supports_quick_actions
and
- = link_to 'quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
+ = link_to _('quick actions'), help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
are
- else
is
@@ -24,12 +24,12 @@
= icon('file-image-o', class: 'toolbar-button-icon')
%span.uploading-error-message
-# Populated by app/assets/javascripts/dropzone_input.js
- %button.retry-uploading-link{ type: 'button' } Try again
+ %button.retry-uploading-link{ type: 'button' }= _("Try again")
or
- %button.attach-new-file.markdown-selector{ type: 'button' } attach a new file
+ %button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file")
%button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file
+ = _("Attach a file")
- %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' } Cancel
+ %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' }= _("Cancel")
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index e125d7f108a..cb5c64fb91d 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -62,7 +62,7 @@
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
.system-note-commit-list-toggler.hide
- Toggle commit list
+ = _("Toggle commit list")
%i.fa.fa-angle-down
- if note.attachment.url
.note-attachment
@@ -74,5 +74,5 @@
= icon('paperclip')
= note.attachment_identifier
= link_to delete_attachment_project_note_path(note.project, note),
- title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
+ title: _('Delete this attachment'), method: :delete, remote: true, data: { confirm: _('Are you sure you want to remove the attachment?') }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 4c4050c6054..002189e6ecd 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -19,20 +19,14 @@
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user
.disabled-comment.text-center.prepend-top-default
- Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link'
- or
- = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link'
- to comment
+ - link_to_register = link_to(_("register"), new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link')
+ - link_to_sign_in = link_to(_("sign in"), new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link')
+ = _("Please %{link_to_register} or %{link_to_sign_in} to comment").html_safe % { link_to_register: link_to_register, link_to_sign_in: link_to_sign_in }
- elsif discussion_locked
.disabled-comment.text-center.prepend-top-default
%span.issuable-note-warning
= sprite_icon('lock', size: 16, css_class: 'icon')
%span
- This
- = issuable.class.to_s.titleize.downcase
- is locked. Only
- %b project members
- can comment.
+ = _("This %{issuable} is locked. Only <strong>project members</strong> can comment.").html_safe % { issuable: issuable.class.to_s.titleize.downcase }
-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index d3cf21db335..223ddc80c88 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -140,3 +140,4 @@
- detect_repository_languages
- repository_cleanup
- delete_stored_files
+- import_issues_csv
diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb
new file mode 100644
index 00000000000..d44fdfec8ae
--- /dev/null
+++ b/app/workers/import_issues_csv_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class ImportIssuesCsvWorker
+ include ApplicationWorker
+
+ sidekiq_retries_exhausted do |job|
+ Upload.find(job['args'][2]).destroy
+ end
+
+ def perform(current_user_id, project_id, upload_id)
+ @user = User.find(current_user_id)
+ @project = Project.find(project_id)
+ @upload = Upload.find(upload_id)
+
+ importer = Issues::ImportCsvService.new(@user, @project, @upload.build_uploader)
+ importer.execute
+
+ @upload.destroy
+ end
+end
diff --git a/changelogs/unreleased/49231-import-issues-csv.yml b/changelogs/unreleased/49231-import-issues-csv.yml
new file mode 100644
index 00000000000..c10bd8143b2
--- /dev/null
+++ b/changelogs/unreleased/49231-import-issues-csv.yml
@@ -0,0 +1,5 @@
+---
+title: Add importing of issues from CSV file
+merge_request: 23532
+author:
+type: added
diff --git a/changelogs/unreleased/53966-hashed-storage-read-only.yml b/changelogs/unreleased/53966-hashed-storage-read-only.yml
new file mode 100644
index 00000000000..2b6c9c49c85
--- /dev/null
+++ b/changelogs/unreleased/53966-hashed-storage-read-only.yml
@@ -0,0 +1,5 @@
+---
+title: 'Hashed Storage: Only set as `read_only` when starting the per-project migration'
+merge_request: 24128
+author:
+type: changed
diff --git a/changelogs/unreleased/gt-externalize-app-views-shared-notes.yml b/changelogs/unreleased/gt-externalize-app-views-shared-notes.yml
new file mode 100644
index 00000000000..39ca6b67a54
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-shared-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/shared/notes`
+merge_request: 23696
+author: Tao Wang
+type: other
diff --git a/config/routes/project.rb b/config/routes/project.rb
index f50bf5ab76f..cf5a57300cf 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -361,6 +361,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
collection do
post :bulk_update
+ post :import_csv
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 3ee32678f34..3e8c218052d 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -85,3 +85,4 @@
- [repository_cleanup, 1]
- [delete_stored_files, 1]
- [remote_mirror_notification, 2]
+ - [import_issues_csv, 2]
diff --git a/db/migrate/20181212171634_create_error_tracking_settings.rb b/db/migrate/20181212171634_create_error_tracking_settings.rb
new file mode 100644
index 00000000000..18c38bd2c47
--- /dev/null
+++ b/db/migrate/20181212171634_create_error_tracking_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateErrorTrackingSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :project_error_tracking_settings, id: :int, primary_key: :project_id, default: nil do |t|
+ t.boolean :enabled, null: false, default: true
+ t.string :api_url, null: false
+ t.string :encrypted_token
+ t.string :encrypted_token_iv
+ t.foreign_key :projects, column: :project_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 12e4ed6d627..87826881d58 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1573,6 +1573,13 @@ ActiveRecord::Schema.define(version: 20190103140724) do
t.index ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
end
+ create_table "project_error_tracking_settings", primary_key: "project_id", id: :integer, force: :cascade do |t|
+ t.boolean "enabled", default: true, null: false
+ t.string "api_url", null: false
+ t.string "encrypted_token"
+ t.string "encrypted_token_iv"
+ end
+
create_table "project_features", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "merge_requests_access_level"
@@ -2434,6 +2441,7 @@ ActiveRecord::Schema.define(version: 20190103140724) do
add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
+ add_foreign_key "project_error_tracking_settings", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
index 621d4f77d5e..0d03b481881 100644
--- a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
@@ -255,7 +255,7 @@ After configuring LDAP, basic authentication will be available. Users can then l
Users that are removed from the LDAP base group (e.g `OU=GitLab INT,DC=GitLab,DC=org`) will be **blocked** in GitLab. [More information](../ldap.md#security) on LDAP security.
-If `allow_username_or_email_login` is enabled in the LDAP configuration, GitLab will ignore everything after the first '@' in the LDAP username used on login. Example: The username `jon.doe@example.com` is converted to `jon.doe` when authenticating with the LDAP server. Disable this setting if you use `userPrincipalName` as the `uid`.
+If `allow_username_or_email_login` is enabled in the LDAP configuration, GitLab will ignore everything after the first '@' in the LDAP username used on login. Example: The username `` jon.doe@example.com `` is converted to `jon.doe` when authenticating with the LDAP server. Disable this setting if you use `userPrincipalName` as the `uid`.
## LDAP extended features on GitLab EE
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index 27a3710632d..05873e01a08 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -13,43 +13,45 @@ GitLab has several features based on receiving incoming emails:
## Requirements
-Handling incoming emails requires an [IMAP]-enabled email account. GitLab
-requires one of the following three strategies:
+Handling incoming emails requires an [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol)-enabled
+email account. GitLab requires one of the following three strategies:
-- Email sub-addressing
-- Dedicated email address
+- Email sub-addressing (recommended)
- Catch-all mailbox
+- Dedicated email address (supports Reply by Email only)
Let's walk through each of these options.
-**If your provider or server supports email sub-addressing, we recommend using it.
-Most features (other than reply by email) only work with sub-addressing.**
-
-[IMAP]: https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol
-
### Email sub-addressing
[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
-a feature where any email to `user+some_arbitrary_tag@example.com` will end up
-in the mailbox for `user@example.com`, and is supported by providers such as
-Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the
-[Postfix mail server] which you can run on-premises.
-
-[Postfix mail server]: reply_by_email_postfix_setup.md
+a mail server feature where any email to `user+arbitrary_tag@example.com` will end up
+in the mailbox for `user@example.com` . It is supported by providers such as
+Gmail, Google Apps, Yahoo! Mail, Outlook.com, and iCloud, as well as the
+[Postfix mail server](reply_by_email_postfix_setup.md), which you can run on-premises.
+
+TIP: **Tip:**
+If your provider or server supports email sub-addressing, we recommend using it.
+A dedicated email address only supports Reply by Email functionality.
+A catch-all mailbox supports the same features as sub-addressing as of GitLab 11.7,
+but sub-addressing is still preferred because only one email address is used,
+leaving a catch-all available for other purposes beyond GitLab.
-### Dedicated email address
+### Catch-all mailbox
-This solution is really simple to set up: you just have to create an email
-address dedicated to receive your users' replies to GitLab notifications.
+A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain
+receives all emails addressed to the domain that do not match any addresses that
+exist on the mail server.
-### Catch-all mailbox
+As of GitLab 11.7, catch-all mailboxes support the same features as
+email sub-addressing, but email sub-addressing remains our recommendation so that you
+can reserve your catch-all mailbox for other purposes.
-A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
-"catch all" the emails addressed to the domain that do not exist in the mail
-server.
+### Dedicated email address
-GitLab can be set up to allow users to comment on issues and merge requests by
-replying to notification emails.
+This solution is relatively simple to set up: you just need to create an email
+address dedicated to receive your users' replies to GitLab notifications. However,
+this method only supports replies, and not the other features of [incoming email](#incoming-email).
## Set it up
@@ -160,14 +162,16 @@ for a real-world example of this exploit.
gitlab_rails['incoming_email_idle_timeout'] = 60
```
- Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes
- mailbox incoming@exchange.example.com
+ Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes the
+ catch-all mailbox incoming@exchange.example.com
```ruby
gitlab_rails['incoming_email_enabled'] = true
- # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
- gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com"
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ # Exchange does not support sub-addressing, so a catch-all mailbox must be used.
+ gitlab_rails['incoming_email_address'] = "incoming-%{key}@exchange.example.com"
# Email account username
# Typically this is the userPrincipalName (UPN)
@@ -279,15 +283,17 @@ for a real-world example of this exploit.
idle_timeout: 60
```
- Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes
- mailbox incoming@exchange.example.com
+ Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes the
+ catch-all mailbox incoming@exchange.example.com
```yaml
incoming_email:
enabled: true
- # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
- address: "incoming@exchange.example.com"
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ # Exchange does not support sub-addressing, so a catch-all mailbox must be used.
+ address: "incoming-%{key}@exchange.example.com"
# Email account username
# Typically this is the userPrincipalName (UPN)
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 877a7af3149..9f552a10589 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -212,7 +212,7 @@ Response:
## Merge Base
-Get the common ancestor for 2 refs (commit SHAs, branch names or tags).
+Get the common ancestor for 2 or more refs (commit SHAs, branch names or tags).
```
GET /projects/:id/repository/merge_base
diff --git a/doc/development/emails.md b/doc/development/emails.md
index 35ada35babe..8baf343b133 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -76,14 +76,32 @@ See the [Rails guides] for more info.
## Email namespace
-If you need to implement a new feature which requires a new email handler, follow these rules:
+As of GitLab 11.7, we support a new format for email handler addresses. This was done to
+support catch-all mailboxes.
- - You must choose a namespace. The namespace cannot contain `/` or `+`, and cannot match `\h{16}`.
- - If your feature is related to a project, you will append the namespace **after** the project path, separated by a `+`
- - If you have different actions in the namespace, you add the actions **after** the namespace separated by a `+`. The action name cannot contain `/` or `+`, , and cannot match `\h{16}`.
- - You will register your handlers in `lib/gitlab/email/handler.rb`
+If you need to implement a feature which requires a new email handler, follow these rules
+for the format of the email key:
-Therefore, these are the only valid formats for an email handler:
+- Actions are always at the end, separated by `-`. For example `-issue` or `-merge-request`
+- If your feature is related to a project, the key begins with the project identifiers (project path slug
+ and project id), separated by `-`. For example, `gitlab-org-gitlab-ce-20`
+- Additional information, such as an author's token, can be added between the project identifiers and
+ the action, separated by `-`. For example, `gitlab-org-gitlab-ce-20-Author_Token12345678-issue`
+- You register your handlers in `lib/gitlab/email/handler.rb`
+
+Examples of valid email keys:
+
+ - `gitlab-org-gitlab-ce-20-Author_Token12345678-issue` (create a new issue)
+ - `gitlab-org-gitlab-ce-20-Author_Token12345678-merge-request` (create a new merge request)
+ - `1234567890abcdef1234567890abcdef-unsubscribe` (unsubscribe from a conversation)
+ - `1234567890abcdef1234567890abcdef` (reply to a conversation)
+
+Please note that the action `-issue-` is used in GitLab Premium as the handler for the Service Desk feature.
+
+### Legacy format
+
+Although we continue to support the older legacy format, no new features should use a legacy format.
+These are the only valid legacy formats for an email handler:
- `path/to/project+namespace`
- `path/to/project+namespace+action`
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 498b702cab1..2d9c7f15634 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -32,7 +32,7 @@ The deployment includes:
## Limitations
[High Availability](../../administration/high_availability/README.md) and
-[Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) are not supported.
+[Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) are not supported.
## Requirements
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 7c7e44d29e7..d34cd1bb1c3 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -241,7 +241,7 @@ Our free SaaS for public and private repositories.
### GitLab Geo
-Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/gitlab-geo/README.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
+Allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational version. It [can be used](https://docs.gitlab.com/ee/administration/geo/replication/index.html) for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
### GitLab High Availability
@@ -303,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use
### Jenkins
-An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins.io/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
+An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins.io/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html).
### Jira
@@ -407,7 +407,7 @@ A free disaster recovery [software](https://help.ubuntu.com/community/MondoMindi
#### Mount
-External reference:
+External reference:
As stated on the [wikipedia page](https://en.wikipedia.org/wiki/Mount_(Unix)), "Mounting makes file systems, files, directories, devices and special files available for use and available to the user."
@@ -447,7 +447,7 @@ Software for which the original source code is freely [available](https://openso
#### Open Source Stewardship
-[Related blog post](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
+[Related blog post](https://about.gitlab.com/2016/01/11/being-a-good-open-source-steward/).
### Owner
@@ -557,7 +557,7 @@ Software that is hosted centrally and accessed on-demand (i.e. whenever you want
This term is often used by people when they mean "Version Control."
-### Scrum
+### Scrum
An Agile [framework](https://www.scrum.org/Resources/What-is-Scrum) designed to typically help complete complex software projects. It's made up of several parts: product requirements backlog, sprint planning, sprint (development), sprint review, and retrospec (analyzing the sprint). The goal is to end up with potentially shippable products.
@@ -697,7 +697,7 @@ A [website/system](http://www.wiki.com/) that allows for collaborative editing o
### Working area
-Files that have been modified but are not committed. Check them by using the command "git status".
+Files that have been modified but are not committed. Check them by using the command "git status".
### Working Tree
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index b21cf27c1d3..2aabbf3be86 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -161,7 +161,7 @@ private subnets.
Now press the Launch a Cache Cluster and choose Redis for our
DB engine. You'll be able to configure details such as replication,
Multi-AZ and node types. The second section will allow us to choose our
-subnet and security group and
+subnet and security group and
![Redis Cluster details](img/redis-cluster-det.png)
@@ -206,7 +206,7 @@ http traffic from anywhere and name it something such as
`gitlab-ec2-security-group`.
While we wait for it to launch we can allocate an Elastic IP and
-associate it with our new EC2 instance.
+associate it with our new EC2 instance.
### RDS and Redis Security Group
@@ -268,8 +268,8 @@ our current case we'll specify the adapter, encoding, host, db name,
username, and password.
gitlab_rails['db_adapter'] = "postgresql"
- gitlab_rails['db_encoding'] = "unicode"
- gitlab_rails['db_database'] = "gitlabhq_production"
+ gitlab_rails['db_encoding'] = "unicode"
+ gitlab_rails['db_database'] = "gitlabhq_production"
gitlab_rails['db_username'] = "gitlab"
gitlab_rails['db_password'] = "mypassword"
gitlab_rails['db_host'] = "<rds-endpoint>"
@@ -288,9 +288,9 @@ to make the EFS integration easier to manage.
Finally, run reconfigure. You might find it useful to run a check and
a service status to make sure everything has been set up correctly.
- sudo gitlab-ctl reconfigure
- sudo gitlab-rake gitlab:check
- sudo gitlab-ctl status
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-rake gitlab:check
+ sudo gitlab-ctl status
If everything looks good copy the Elastic IP over to your browser and
test the instance manually.
@@ -396,4 +396,4 @@ There is a lot of ground yet to cover so have a read through these other
resources and feel free to open an issue to request additional material.
* [GitLab High Availability](http://docs.gitlab.com/ce/administration/high_availability/README.html#sts=High%20Availability)
-* [GitLab Geo](http://docs.gitlab.com/ee/gitlab-geo/README.html)
+* [GitLab Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html)
diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md
index c33d1365001..2bf4fa287e9 100644
--- a/doc/user/project/issues/create_new_issue.md
+++ b/doc/user/project/issues/create_new_issue.md
@@ -39,34 +39,40 @@ It opens a new issue for that project labeled after its respective list.
## New issue via email
-*This feature needs [incoming email](../../../administration/incoming_email.md)
-to be configured by a GitLab administrator to be available for CE/EE users, and
-it's available on GitLab.com.*
+At the bottom of a project's Issues List page, a link to **Email a new issue to this project**
+is displayed if your GitLab instance has [incoming email](../../../administration/incoming_email.md) configured.
-At the bottom of a project's issue page, click
-**Email a new issue to this project**, and you will find an email address
-which belongs to you. You could add this address to your contact.
+![Bottom of a project issues page](img/new_issue_from_email.png)
+
+When you click this link, an email address is displayed which belongs to you for creating issues in this project.
+You can save this address as a contact in your email client for easy acceess.
-This is a private email address, generated just for you.
-**Keep it to yourself** as anyone who gets ahold of it can create issues or
-merge requests as if they were you. You can add this address to your contact
-list for easy access.
+CAUTION: **Caution:**
+This is a private email address, generated just for you. **Keep it to yourself**,
+as anyone who gets ahold of it can create issues or merge requests as if they
+were you. If the address is compromised, or you'd like it to be regenerated for
+any reason, click **Email a new issue to this project** again and click the reset link.
Sending an email to this address will create a new issue on your behalf for
-this project, where the email subject becomes the issue title, and the email
-body becomes the issue description. [Markdown] and [quick actions] are
-supported.
+this project, where:
-![Bottom of a project issues page](img/new_issue_from_email.png)
+- The email subject becomes the issue title.
+- The email body becomes the issue description.
+- [Markdown](../../markdown.md) and [quick actions](../quick_actions.md) are supported.
+
+NOTE: **Note:**
+In GitLab 11.7, we updated the format of the generated email address.
+However the older format is still supported, allowing existing aliases
+or contacts to continue working._
## New issue via URL with prefilled fields
You can link directly to the new issue page for a given project, with prefilled
-field values using query string parameters in a URL. This is useful for embedding
-a URL in an external HTML page, and also certain scenarios where you want the user to
+field values using query string parameters in a URL. This is useful for embedding
+a URL in an external HTML page, and also certain scenarios where you want the user to
create an issue with certain fields prefilled.
-The title, description, and description template fields can be prefilled using
+The title, description, and description template fields can be prefilled using
this method. The description and description template fields cannot be pre-entered
in the same URL (since a description template just populates the description field).
diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md
new file mode 100644
index 00000000000..001e0d303e9
--- /dev/null
+++ b/doc/user/project/issues/csv_import.md
@@ -0,0 +1,45 @@
+# Importing Issues from CSV
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23532) in GitLab 11.7.
+
+Issues can be imported by uploading a CSV file. The file will be processed in the background and a notification email
+will be sent to you once the import is completed.
+
+> **Note:** A permission level of `Developer` or higher is required to import issues.
+
+## CSV File Format
+
+### Header row
+
+CSV files must contain a header row with at least two columns: `title` and `description`, in that order.
+
+### Column separator
+
+The column separator is automatically detected from the header row.
+
+Supported separator characters are: commas (`,`), semicolons (`;`), and tabs (`\t`).
+
+### Row separator
+
+Lines ending in either `CRLF` or `LF` are supported.
+
+### Quote character
+
+The double-quote (`"`) character is used to quote fields so you can use the column separator within a field. To insert
+a double-quote (`"`) within a quoted field, use two double-quote characters in succession, i.e. `""`.
+
+### Data rows
+
+After the header row, succeeding rows must follow the same column order. The issue title is required while the
+description is optional.
+
+The user uploading the CSV file will be set as the author of the imported issues.
+
+## Sample Data
+
+```csv
+title,description
+My Issue Title,My Issue Description
+Another Title,"A description, with a comma"
+"One More Title","One More Description"
+```
diff --git a/doc/user/project/issues/img/import_csv_button.png b/doc/user/project/issues/img/import_csv_button.png
new file mode 100644
index 00000000000..ab100a95750
--- /dev/null
+++ b/doc/user/project/issues/img/import_csv_button.png
Binary files differ
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 200b3a642a1..40a1f60c4ab 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -142,6 +142,15 @@ to find out more about this feature.
With [GitLab Starter](https://about.gitlab.com/pricing/), you can also
create various boards per project with [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards).
+### Import Issues from CSV
+
+From the project-level issues list, you can find the import button near the "Edit issues" button in the upper-right
+side.
+
+![Import CSV button](img/import_csv_button.png)
+
+Learn more about [importing issues from CSV](csv_import.md)
+
### External Issue Tracker
Alternatively to GitLab's built-in Issue Tracker, you can also use an [external
diff --git a/doc/user/project/merge_requests/img/create_from_email.png b/doc/user/project/merge_requests/img/create_from_email.png
index 610f0b3d0c1..5cb2afaf976 100644
--- a/doc/user/project/merge_requests/img/create_from_email.png
+++ b/doc/user/project/merge_requests/img/create_from_email.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index d4f8cf929f6..a19989afb91 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -169,9 +169,9 @@ those conflicts in the GitLab UI.
## Create new merge requests by email
-*This feature needs [incoming email](../../../administration/incoming_email.md)
+_This feature needs [incoming email](../../../administration/incoming_email.md)
to be configured by a GitLab administrator to be available for CE/EE users, and
-it's available on GitLab.com.*
+it's available on GitLab.com._
You can create a new merge request by sending an email to a user-specific email
address. The address can be obtained on the merge requests page by clicking on
@@ -183,8 +183,16 @@ will be used as the merge request description. You need
this feature. If it's not enabled to your instance, you may ask your GitLab
administrator to do so.
+This is a private email address, generated just for you. **Keep it to yourself**
+as anyone who gets ahold of it can create issues or merge requests as if they were you.
+You can add this address to your contact list for easy access.
+
![Create new merge requests by email](img/create_from_email.png)
+_In GitLab 11.7, we updated the format of the generated email address.
+However the older format is still supported, allowing existing aliases
+or contacts to continue working._
+
### Adding patches when creating a merge request via e-mail
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22723) in GitLab 11.5.
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index f5d21d8923f..9f3a1699146 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -475,7 +475,7 @@ module API
requires :file, type: File, desc: 'The file to be uploaded'
end
post ":id/uploads" do
- UploadService.new(user_project, params[:file]).execute
+ UploadService.new(user_project, params[:file]).execute.to_h
end
desc 'Get the users list of a project' do
diff --git a/lib/gitlab/email/attachment_uploader.rb b/lib/gitlab/email/attachment_uploader.rb
index a826519b2dd..3323ce60158 100644
--- a/lib/gitlab/email/attachment_uploader.rb
+++ b/lib/gitlab/email/attachment_uploader.rb
@@ -23,8 +23,8 @@ module Gitlab
content_type: attachment.content_type
}
- link = UploadService.new(project, file).execute
- attachments << link if link
+ uploader = UploadService.new(project, file).execute
+ attachments << uploader.to_h if uploader
ensure
tmp.close!
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index a1a374cef4a..7987533978c 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -74,6 +74,7 @@ project_tree:
- :prometheus_metrics
- :project_badges
- :ci_cd_settings
+ - :error_tracking_setting
# Only include the following attributes for the models specified.
included_attributes:
@@ -162,6 +163,9 @@ excluded_attributes:
- :token_encrypted
services:
- :template
+ error_tracking_setting:
+ - :encrypted_token
+ - :encrypted_token_iv
methods:
labels:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index bce12103cce..099b488f68e 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -24,6 +24,7 @@ module Gitlab
project_badges: 'Badge',
metrics: 'MergeRequest::Metrics',
ci_cd_settings: 'ProjectCiCdSetting',
+ error_tracking_setting: 'ErrorTracking::ProjectErrorTrackingSetting',
links: 'Releases::Link' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
diff --git a/lib/gitlab/import_export/uploads_manager.rb b/lib/gitlab/import_export/uploads_manager.rb
index 474e9d45566..e232198150a 100644
--- a/lib/gitlab/import_export/uploads_manager.rb
+++ b/lib/gitlab/import_export/uploads_manager.rb
@@ -40,7 +40,7 @@ module Gitlab
def add_upload(upload)
uploader_context = FileUploader.extract_dynamic_path(upload).named_captures.symbolize_keys
- UploadService.new(@project, File.open(upload, 'r'), FileUploader, uploader_context).execute
+ UploadService.new(@project, File.open(upload, 'r'), FileUploader, uploader_context).execute.to_h
end
def copy_project_uploads
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 44c71f8431d..9b7b0db9525 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -8,16 +8,18 @@ module Gitlab
BlockedUrlError = Class.new(StandardError)
class << self
- def validate!(url, ports: [], protocols: [], allow_localhost: false, allow_local_network: true, ascii_only: false, enforce_user: false)
+ def validate!(url, ports: [], protocols: [], allow_localhost: false, allow_local_network: true, ascii_only: false, enforce_user: false, enforce_sanitization: false)
return true if url.nil?
# Param url can be a string, URI or Addressable::URI
uri = parse_url(url)
+ validate_html_tags!(uri) if enforce_sanitization
+
# Allow imports from the GitLab instance itself but only from the configured ports
return true if internal?(uri)
- port = uri.port || uri.default_port
+ port = get_port(uri)
validate_protocol!(uri.scheme, protocols)
validate_port!(port, ports) if ports.any?
validate_user!(uri.user) if enforce_user
@@ -50,6 +52,18 @@ module Gitlab
private
+ def get_port(uri)
+ uri.port || uri.default_port
+ end
+
+ def validate_html_tags!(uri)
+ uri_str = uri.to_s
+ sanitized_uri = ActionController::Base.helpers.sanitize(uri_str, tags: [])
+ if sanitized_uri != uri_str
+ raise BlockedUrlError, 'HTML/CSS/JS tags are not allowed'
+ end
+ end
+
def parse_url(url)
raise Addressable::URI::InvalidURIError if multiline?(url)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 092c37d96bf..7db75b5cfef 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -372,6 +372,9 @@ msgstr ""
msgid "Add README"
msgstr ""
+msgid "Add a general comment to this %{noteable_name}."
+msgstr ""
+
msgid "Add a homepage to your wiki that contains information about your project and GitLab will display it here instead of this message."
msgstr ""
@@ -705,6 +708,9 @@ msgstr ""
msgid "Are you sure you want to remove %{group_name}?"
msgstr ""
+msgid "Are you sure you want to remove the attachment?"
+msgstr ""
+
msgid "Are you sure you want to remove this identity?"
msgstr ""
@@ -759,6 +765,9 @@ msgstr ""
msgid "Assignee(s)"
msgstr ""
+msgid "Attach a file"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
@@ -768,6 +777,9 @@ msgstr ""
msgid "August"
msgstr ""
+msgid "Auth Token"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -1890,6 +1902,12 @@ msgstr ""
msgid "Comment"
msgstr ""
+msgid "Comment & close %{noteable_name}"
+msgstr ""
+
+msgid "Comment & reopen %{noteable_name}"
+msgstr ""
+
msgid "Comment & resolve discussion"
msgstr ""
@@ -2387,6 +2405,9 @@ msgstr ""
msgid "Delete list"
msgstr ""
+msgid "Delete this attachment"
+msgstr ""
+
msgid "Deleted"
msgstr ""
@@ -2599,6 +2620,12 @@ msgstr ""
msgid "Discover projects, groups and snippets. Share your projects with others"
msgstr ""
+msgid "Discuss a specific suggestion or question"
+msgstr ""
+
+msgid "Discuss a specific suggestion or question that needs to be resolved"
+msgstr ""
+
msgid "Dismiss"
msgstr ""
@@ -2686,6 +2713,9 @@ msgstr ""
msgid "Edit identity for %{user_name}"
msgstr ""
+msgid "Edit issues"
+msgstr ""
+
msgid "Email"
msgstr ""
@@ -2761,6 +2791,9 @@ msgstr ""
msgid "Enter the merge request title"
msgstr ""
+msgid "Enter your Sentry API URL"
+msgstr ""
+
msgid "Environment variables"
msgstr ""
@@ -2863,6 +2896,9 @@ msgstr ""
msgid "Error Reporting and Logging"
msgstr ""
+msgid "Error Tracking"
+msgstr ""
+
msgid "Error fetching contributors data."
msgstr ""
@@ -3070,6 +3106,9 @@ msgstr ""
msgid "File templates"
msgstr ""
+msgid "File upload error."
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -3085,6 +3124,9 @@ msgstr ""
msgid "Filter..."
msgstr ""
+msgid "Find and manage Auth Tokens in your Sentry account settings page."
+msgstr ""
+
msgid "Find by path"
msgstr ""
@@ -3100,6 +3142,9 @@ msgstr ""
msgid "Fingerprints"
msgstr ""
+msgid "Finish editing this message first!"
+msgstr ""
+
msgid "Finished"
msgstr ""
@@ -3576,6 +3621,9 @@ msgstr ""
msgid "Import"
msgstr ""
+msgid "Import CSV"
+msgstr ""
+
msgid "Import Projects from Gitea"
msgstr ""
@@ -3594,6 +3642,9 @@ msgstr ""
msgid "Import in progress"
msgstr ""
+msgid "Import issues"
+msgstr ""
+
msgid "Import multiple repositories by uploading a manifest file."
msgstr ""
@@ -3732,6 +3783,9 @@ msgstr ""
msgid "Issues, merge requests, pushes and comments."
msgstr ""
+msgid "It must have a header row and at least two columns: the first column is the issue title and the second column is the issue description. The separator is automatically detected."
+msgstr ""
+
msgid "It's you"
msgstr ""
@@ -4066,6 +4120,9 @@ msgstr ""
msgid "Mark todo as done"
msgstr ""
+msgid "Markdown"
+msgstr ""
+
msgid "Markdown enabled"
msgstr ""
@@ -4664,6 +4721,9 @@ msgstr ""
msgid "Open Documentation"
msgstr ""
+msgid "Open comment type dropdown"
+msgstr ""
+
msgid "Open in Xcode"
msgstr ""
@@ -4958,6 +5018,9 @@ msgstr ""
msgid "Play"
msgstr ""
+msgid "Please %{link_to_register} or %{link_to_sign_in} to comment"
+msgstr ""
+
msgid "Please accept the Terms of Service before continuing."
msgstr ""
@@ -5820,6 +5883,9 @@ msgstr ""
msgid "Save changes"
msgstr ""
+msgid "Save comment"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr ""
@@ -5973,6 +6039,9 @@ msgstr ""
msgid "Send usage data"
msgstr ""
+msgid "Sentry API URL"
+msgstr ""
+
msgid "Sep"
msgstr ""
@@ -6380,6 +6449,15 @@ msgstr ""
msgid "Start date"
msgstr ""
+msgid "Start discussion"
+msgstr ""
+
+msgid "Start discussion & close %{noteable_name}"
+msgstr ""
+
+msgid "Start discussion & reopen %{noteable_name}"
+msgstr ""
+
msgid "Start the Runner!"
msgstr ""
@@ -6443,6 +6521,12 @@ msgstr ""
msgid "Subscribe at project level"
msgstr ""
+msgid "Subscribe to RSS feed"
+msgstr ""
+
+msgid "Subscribe to calendar"
+msgstr ""
+
msgid "Subscribed"
msgstr ""
@@ -6602,7 +6686,7 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
-msgid "The maximum file size allowed is %{max_attachment_size}mb"
+msgid "The maximum file size allowed is %{size}."
msgstr ""
msgid "The maximum file size allowed is 200KB."
@@ -6716,6 +6800,9 @@ msgstr ""
msgid "Third party offers"
msgstr ""
+msgid "This %{issuable} is locked. Only <strong>project members</strong> can comment."
+msgstr ""
+
msgid "This %{viewer} could not be displayed because %{reason}. You can %{options} instead."
msgstr ""
@@ -7101,6 +7188,9 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
+msgid "To link Sentry to GitLab, enter your Sentry URL and Auth Token."
+msgstr ""
+
msgid "To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here."
msgstr ""
@@ -7140,6 +7230,9 @@ msgstr ""
msgid "Toggle commit description"
msgstr ""
+msgid "Toggle commit list"
+msgstr ""
+
msgid "Toggle discussion"
msgstr ""
@@ -7290,6 +7383,9 @@ msgstr ""
msgid "Upload <code>GoogleCodeProjectHosting.json</code> here:"
msgstr ""
+msgid "Upload CSV file"
+msgstr ""
+
msgid "Upload New File"
msgstr ""
@@ -7644,6 +7740,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
+msgid "Write a comment or drag your files here…"
+msgstr ""
+
msgid "Yes"
msgstr ""
@@ -7836,6 +7935,12 @@ msgstr ""
msgid "Your groups"
msgstr ""
+msgid "Your issues are being imported. Once finished, you'll get a confirmation email."
+msgstr ""
+
+msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
+msgstr ""
+
msgid "Your name"
msgstr ""
@@ -7854,6 +7959,9 @@ msgstr ""
msgid "assign yourself"
msgstr ""
+msgid "attach a new file"
+msgstr ""
+
msgid "branch name"
msgstr ""
@@ -7918,6 +8026,9 @@ msgstr ""
msgid "here"
msgstr ""
+msgid "http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/issues/"
+msgstr ""
+
msgid "https://your-bitbucket-server"
msgstr ""
@@ -8182,6 +8293,12 @@ msgstr ""
msgid "project"
msgstr ""
+msgid "quick actions"
+msgstr ""
+
+msgid "register"
+msgstr ""
+
msgid "remaining"
msgstr ""
@@ -8202,6 +8319,9 @@ msgstr ""
msgid "show less"
msgstr ""
+msgid "sign in"
+msgstr ""
+
msgid "source"
msgstr ""
diff --git a/package.json b/package.json
index bc7bc0880de..ff1140128f8 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"@babel/preset-env": "^7.1.0",
"@gitlab/csslab": "^1.8.0",
"@gitlab/svgs": "^1.47.0",
- "@gitlab/ui": "^1.18.0",
+ "@gitlab/ui": "^1.20.0",
"apollo-boost": "^0.1.20",
"apollo-client": "^2.4.5",
"autosize": "^4.0.0",
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 6cd5c06a088..5ee8df03d50 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -10,37 +10,35 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
end
- before(:all) do
- login
+ [true, false].each do |rbac|
+ context "when rbac is #{rbac ? 'enabled' : 'disabled'}" do
+ before(:all) do
+ login
- @project = Resource::Project.fabricate! do |p|
- p.name = Runtime::Env.auto_devops_project_name || 'project-with-autodevops'
- p.description = 'Project with Auto Devops'
- end
+ @project = Resource::Project.fabricate! do |p|
+ p.name = Runtime::Env.auto_devops_project_name || 'project-with-autodevops'
+ p.description = 'Project with Auto DevOps'
+ end
- # Disable code_quality check in Auto DevOps pipeline as it takes
- # too long and times out the test
- Resource::CiVariable.fabricate! do |resource|
- resource.project = @project
- resource.key = 'CODE_QUALITY_DISABLED'
- resource.value = '1'
- end
+ # Disable code_quality check in Auto DevOps pipeline as it takes
+ # too long and times out the test
+ Resource::CiVariable.fabricate! do |resource|
+ resource.project = @project
+ resource.key = 'CODE_QUALITY_DISABLED'
+ resource.value = '1'
+ end
- # Create Auto Devops compatible repo
- Resource::Repository::ProjectPush.fabricate! do |push|
- push.project = @project
- push.directory = Pathname
- .new(__dir__)
- .join('../../../../../fixtures/auto_devops_rack')
- push.commit_message = 'Create Auto DevOps compatible rack application'
- end
+ # Create Auto DevOps compatible repo
+ Resource::Repository::ProjectPush.fabricate! do |push|
+ push.project = @project
+ push.directory = Pathname
+ .new(__dir__)
+ .join('../../../../../fixtures/auto_devops_rack')
+ push.commit_message = 'Create Auto DevOps compatible rack application'
+ end
- Page::Project::Show.act { wait_for_push }
- end
+ Page::Project::Show.act { wait_for_push }
- [true, false].each do |rbac|
- context "when rbac is #{rbac ? 'enabled' : 'disabled'}" do
- before(:all) do
# Create and connect K8s cluster
@cluster = Service::KubernetesCluster.new(rbac: rbac).create!
kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster|
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index a239ac16c0d..df21dc7bc85 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1026,6 +1026,72 @@ describe Projects::IssuesController do
end
end
+ describe 'POST #import_csv' do
+ let(:project) { create(:project, :public) }
+ let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }
+
+ context 'feature disabled' do
+ it 'returns 404' do
+ sign_in(user)
+ project.add_maintainer(user)
+
+ stub_feature_flags(issues_import_csv: false)
+
+ import_csv
+
+ expect(response).to have_gitlab_http_status :not_found
+ end
+ end
+
+ context 'unauthorized' do
+ it 'returns 404 for guests' do
+ sign_out(:user)
+
+ import_csv
+
+ expect(response).to have_gitlab_http_status :not_found
+ end
+
+ it 'returns 404 for project members with reporter role' do
+ sign_in(user)
+ project.add_reporter(user)
+
+ import_csv
+
+ expect(response).to have_gitlab_http_status :not_found
+ end
+ end
+
+ context 'authorized' do
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ it "returns 302 for project members with developer role" do
+ import_csv
+
+ expect(flash[:notice]).to include('Your issues are being imported')
+ expect(response).to redirect_to(project_issues_path(project))
+ end
+
+ it "shows error when upload fails" do
+ allow_any_instance_of(UploadService).to receive(:execute).and_return(nil)
+
+ import_csv
+
+ expect(flash[:alert]).to include('File upload error.')
+ expect(response).to redirect_to(project_issues_path(project))
+ end
+ end
+
+ def import_csv
+ post :import_csv, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ file: file
+ end
+ end
+
describe 'GET #discussions' do
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
context 'when authenticated' do
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index fbb26de76d1..810f5bb64ba 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -11,25 +11,171 @@ describe Projects::Settings::OperationsController do
project.add_maintainer(user)
end
- describe 'GET #show' do
- it 'returns 404' do
- get :show, params: project_params(project)
+ context 'error tracking' do
+ describe 'GET #show' do
+ it 'renders show template' do
+ get :show, params: project_params(project)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+
+ context 'with existing setting' do
+ let!(:error_tracking_setting) do
+ create(:project_error_tracking_setting, project: project)
+ end
+
+ it 'loads existing setting' do
+ get :show, params: project_params(project)
+
+ expect(controller.helpers.error_tracking_setting)
+ .to eq(error_tracking_setting)
+ end
+ end
+
+ context 'without an existing setting' do
+ it 'builds a new setting' do
+ get :show, params: project_params(project)
+
+ expect(controller.helpers.error_tracking_setting).to be_new_record
+ end
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(error_tracking: false)
+ end
+
+ it 'renders 404' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
+ get :show, params: project_params(project)
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ let(:operations_update_service) { spy(:operations_update_service) }
+ let(:operations_url) { project_settings_operations_url(project) }
+
+ let(:error_tracking_params) do
+ {
+ error_tracking_setting_attributes: {
+ enabled: '1',
+ api_url: 'http://url',
+ token: 'token'
+ }
+ }
+ end
+ let(:error_tracking_permitted) do
+ ActionController::Parameters.new(error_tracking_params).permit!
+ end
+
+ context 'when update succeeds' do
+ before do
+ stub_operations_update_service_returning(status: :success)
+ end
+
+ it 'shows a notice' do
+ patch :update, params: project_params(project, error_tracking_params)
+
+ expect(response).to redirect_to(operations_url)
+ expect(flash[:notice]).to eq _('Your changes have been saved')
+ end
+ end
+
+ context 'when update fails' do
+ before do
+ stub_operations_update_service_returning(status: :error)
+ end
+
+ it 'renders show page' do
+ patch :update, params: project_params(project, error_tracking_params)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(error_tracking: false)
+ end
+
+ it 'renders 404' do
+ patch :update, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ patch :update, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
+ patch :update, params: project_params(project)
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
end
- end
- describe 'PATCH #update' do
- it 'returns 404' do
- patch :update, params: project_params(project)
+ private
- expect(response).to have_gitlab_http_status(:not_found)
+ def stub_operations_update_service_returning(return_value = {})
+ expect(::Projects::Operations::UpdateService)
+ .to receive(:new).with(project, user, error_tracking_permitted)
+ .and_return(operations_update_service)
+ expect(operations_update_service).to receive(:execute)
+ .and_return(return_value)
end
end
private
- def project_params(project)
- { namespace_id: project.namespace, project_id: project }
+ def project_params(project, params = {})
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: params
+ }
end
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 7c505ee0d43..897b4411055 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -64,6 +64,7 @@ describe 'Database schema' do
let(:indexes) { connection.indexes(table) }
let(:columns) { connection.columns(table) }
let(:foreign_keys) { connection.foreign_keys(table) }
+ let(:primary_key_column) { connection.primary_key(table) }
context 'all foreign keys' do
# for index to be effective, the FK constraint has to be at first place
@@ -71,6 +72,12 @@ describe 'Database schema' do
first_indexed_column = indexes.map(&:columns).map(&:first)
foreign_keys_columns = foreign_keys.map(&:column)
+ # Add the primary key column to the list of indexed columns because
+ # postgres and mysql both automatically create an index on the primary
+ # key. Also, the rails connection.indexes() method does not return
+ # automatically generated indexes (like the primary key index).
+ first_indexed_column = first_indexed_column.push(primary_key_column)
+
expect(first_indexed_column.uniq).to include(*foreign_keys_columns)
end
end
diff --git a/spec/factories/project_error_tracking_settings.rb b/spec/factories/project_error_tracking_settings.rb
new file mode 100644
index 00000000000..f044cbe8755
--- /dev/null
+++ b/spec/factories/project_error_tracking_settings.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_error_tracking_setting, class: ErrorTracking::ProjectErrorTrackingSetting do
+ project
+ api_url 'https://gitlab.com'
+ enabled true
+ token 'access_token_123'
+ end
+end
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
new file mode 100644
index 00000000000..1f2328a6dd8
--- /dev/null
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Projects > Settings > For a forked project', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:role) { :maintainer }
+
+ before do
+ stub_feature_flags(error_tracking: true)
+ sign_in(user)
+ project.add_role(user, role)
+ end
+
+ describe 'Sidebar > Operations' do
+ context 'when sidebar feature flag enabled' do
+ it 'renders the settings link in the sidebar' do
+ visit project_path(project)
+ wait_for_requests
+
+ expect(page).to have_selector('a[title="Operations"]', visible: false)
+ end
+ end
+
+ context 'when sidebar feature flag disabled' do
+ before do
+ stub_feature_flags(error_tracking: false)
+ end
+
+ it 'does not render the settings link in the sidebar' do
+ visit project_path(project)
+ wait_for_requests
+
+ expect(page).not_to have_selector('a[title="Operations"]', visible: false)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/csv_comma.csv b/spec/fixtures/csv_comma.csv
new file mode 100644
index 00000000000..e477a27d243
--- /dev/null
+++ b/spec/fixtures/csv_comma.csv
@@ -0,0 +1,4 @@
+title,description
+Issue in 中文,Test description
+"Hello","World"
+"Title with quote""",Description
diff --git a/spec/fixtures/csv_semicolon.csv b/spec/fixtures/csv_semicolon.csv
new file mode 100644
index 00000000000..679797489e2
--- /dev/null
+++ b/spec/fixtures/csv_semicolon.csv
@@ -0,0 +1,5 @@
+title;description
+Issue in 中文;Test description
+Title with, comma;"Description"
+
+"Hello";"World"
diff --git a/spec/fixtures/csv_tab.csv b/spec/fixtures/csv_tab.csv
new file mode 100644
index 00000000000..f801794ea9c
--- /dev/null
+++ b/spec/fixtures/csv_tab.csv
@@ -0,0 +1,4 @@
+title description
+Issue in 中文 Test description
+ "Error Row"
+"Hello" "World"
diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json
index 500c19e3abb..c087352a122 100644
--- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json
+++ b/spec/fixtures/security-reports/master/gl-container-scanning-report.json
@@ -1,18 +1,92 @@
{
- "image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583",
+ "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff",
"unapproved": [
- "CVE-2017-15651"
+ "CVE-2017-18018",
+ "CVE-2016-2781",
+ "CVE-2017-12424",
+ "CVE-2007-5686",
+ "CVE-2013-4235"
],
"vulnerabilities": [
{
- "featurename": "musl",
- "featureversion": "1.1.14-r15",
- "vulnerability": "CVE-2017-15651",
- "namespace": "alpine:v3.4",
- "description": "",
- "link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15651",
+ "featurename": "glibc",
+ "featureversion": "2.24-11+deb9u3",
+ "vulnerability": "CVE-2017-18269",
+ "namespace": "debian:9",
+ "description": "SSE2-optimized memmove implementation problem.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2017-18269",
+ "severity": "Defcon1",
+ "fixedby": "2.24-11+deb9u4"
+ },
+ {
+ "featurename": "glibc",
+ "featureversion": "2.24-11+deb9u3",
+ "vulnerability": "CVE-2017-16997",
+ "namespace": "debian:9",
+ "description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2017-16997",
+ "severity": "Critical",
+ "fixedby": ""
+ },
+ {
+ "featurename": "glibc",
+ "featureversion": "2.24-11+deb9u3",
+ "vulnerability": "CVE-2018-1000001",
+ "namespace": "debian:9",
+ "description": "In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2018-1000001",
+ "severity": "High",
+ "fixedby": ""
+ },
+ {
+ "featurename": "glibc",
+ "featureversion": "2.24-11+deb9u3",
+ "vulnerability": "CVE-2016-10228",
+ "namespace": "debian:9",
+ "description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.25 and earlier, when invoked with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
"severity": "Medium",
- "fixedby": "1.1.14-r16"
+ "fixedby": ""
+ },
+ {
+ "featurename": "elfutils",
+ "featureversion": "0.168-1",
+ "vulnerability": "CVE-2018-18520",
+ "namespace": "debian:9",
+ "description": "An Invalid Memory Address Dereference exists in the function elf_end in libelf in elfutils through v0.174. Although eu-size is intended to support ar files inside ar files, handle_ar in size.c closes the outer ar file before handling all inner entries. The vulnerability allows attackers to cause a denial of service (application crash) with a crafted ELF file.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2018-18520",
+ "severity": "Low",
+ "fixedby": ""
+ },
+ {
+ "featurename": "glibc",
+ "featureversion": "2.24-11+deb9u3",
+ "vulnerability": "CVE-2010-4052",
+ "namespace": "debian:9",
+ "description": "Stack consumption vulnerability in the regcomp implementation in the GNU C Library (aka glibc or libc6) through 2.11.3, and 2.12.x through 2.12.2, allows context-dependent attackers to cause a denial of service (resource exhaustion) via a regular expression containing adjacent repetition operators, as demonstrated by a {10,}{10,}{10,}{10,} sequence in the proftpd.gnu.c exploit for ProFTPD.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2010-4052",
+ "severity": "Negligible",
+ "fixedby": ""
+ },
+ {
+ "featurename": "nettle",
+ "featureversion": "3.3-1",
+ "vulnerability": "CVE-2018-16869",
+ "namespace": "debian:9",
+ "description": "A Bleichenbacher type side-channel based padding oracle attack was found in the way nettle handles endian conversion of RSA decrypted PKCS#1 v1.5 data. An attacker who is able to run a process on the same physical core as the victim process, could use this flaw extract plaintext or in some cases downgrade any TLS connections to a vulnerable server.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2018-16869",
+ "severity": "Unknown",
+ "fixedby": ""
+ },
+ {
+ "featurename": "perl",
+ "featureversion": "5.24.1-3+deb9u4",
+ "vulnerability": "CVE-2018-18311",
+ "namespace": "debian:9",
+ "description": "Perl before 5.26.3 and 5.28.x before 5.28.1 has a buffer overflow via a crafted regular expression that triggers invalid write operations.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311",
+ "severity": "Unknown",
+ "fixedby": "5.24.1-3+deb9u5"
}
]
-}
+} \ No newline at end of file
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index 6f414c8ccf1..a14031f43ed 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -205,4 +205,40 @@ describe('GfmAutoComplete', function() {
expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false);
});
});
+
+ describe('Issues.insertTemplateFunction', function() {
+ it('should return default template', function() {
+ expect(GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, title: 'Some Issue' })).toBe(
+ '${atwho-at}${id}', // eslint-disable-line no-template-curly-in-string
+ );
+ });
+
+ it('should return reference when reference is set', function() {
+ expect(
+ GfmAutoComplete.Issues.insertTemplateFunction({
+ id: 5,
+ title: 'Some Issue',
+ reference: 'grp/proj#5',
+ }),
+ ).toBe('grp/proj#5');
+ });
+ });
+
+ describe('Issues.templateFunction', function() {
+ it('should return html with id and title', function() {
+ expect(GfmAutoComplete.Issues.templateFunction({ id: 5, title: 'Some Issue' })).toBe(
+ '<li><small>5</small> Some Issue</li>',
+ );
+ });
+
+ it('should replace id with reference if reference is set', function() {
+ expect(
+ GfmAutoComplete.Issues.templateFunction({
+ id: 5,
+ title: 'Some Issue',
+ reference: 'grp/proj#5',
+ }),
+ ).toBe('<li><small>grp/proj#5</small> Some Issue</li>');
+ });
+ });
});
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
index 7eac2cacb90..01d43ed00a2 100644
--- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -19,15 +19,6 @@ describe Gitlab::HashedStorage::Migrator do
end
end
- it 'sets projects as read only' do
- allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
- subject.bulk_migrate(ids.min, ids.max)
-
- projects.each do |project|
- expect(project.reload.repository_read_only?).to be_truthy
- end
- end
-
it 'rescues and log exceptions' do
allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
expect { subject.bulk_migrate(ids.min, ids.max) }.not_to raise_error
@@ -40,6 +31,16 @@ describe Gitlab::HashedStorage::Migrator do
subject.bulk_migrate(ids.min, ids.max)
end
+
+ it 'has migrated projects set as writable' do
+ perform_enqueued_jobs do
+ subject.bulk_migrate(ids.min, ids.max)
+ end
+
+ projects.each do |project|
+ expect(project.reload.repository_read_only?).to be_falsey
+ end
+ end
end
describe '#migrate' do
@@ -57,19 +58,20 @@ describe Gitlab::HashedStorage::Migrator do
expect { subject.migrate(project) }.not_to raise_error
end
- it 'sets project as read only' do
- allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async)
- subject.migrate(project)
+ it 'migrate project' do
+ perform_enqueued_jobs do
+ subject.migrate(project)
+ end
- expect(project.reload.repository_read_only?).to be_truthy
+ expect(project.reload.hashed_storage?(:attachments)).to be_truthy
end
- it 'migrate project' do
+ it 'has migrated project set as writable' do
perform_enqueued_jobs do
subject.migrate(project)
end
- expect(project.reload.hashed_storage?(:attachments)).to be_truthy
+ expect(project.reload.repository_read_only?).to be_falsey
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index d3cae137c3c..5afa9669b1a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -314,6 +314,7 @@ project:
- repository_languages
- pool_repository
- kubernetes_namespaces
+- error_tracking_setting
award_emoji:
- awardable
- user
@@ -345,3 +346,5 @@ resource_label_events:
- merge_request
- epic
- label
+error_tracking_setting:
+- project
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 2422868474e..fe2087e8fc3 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -600,3 +600,8 @@ ResourceLabelEvent:
- label_id
- user_id
- created_at
+ErrorTracking::ProjectErrorTrackingSetting:
+- id
+- api_url
+- enabled
+- project_id
diff --git a/spec/mailers/emails/issues_spec.rb b/spec/mailers/emails/issues_spec.rb
new file mode 100644
index 00000000000..09253cf8003
--- /dev/null
+++ b/spec/mailers/emails/issues_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+describe Emails::Issues do
+ include EmailSpec::Matchers
+
+ describe "#import_issues_csv_email" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ subject { Notify.import_issues_csv_email(user.id, project.id, @results) }
+
+ it "shows number of successful issues imported" do
+ @results = { success: 165, error_lines: [], parse_error: false }
+
+ expect(subject).to have_body_text "165 issues imported"
+ end
+
+ it "shows error when file is invalid" do
+ @results = { success: 0, error_lines: [], parse_error: true }
+
+ expect(subject).to have_body_text "Error parsing CSV"
+ end
+
+ it "shows line numbers with errors" do
+ @results = { success: 0, error_lines: [23, 34, 58], parse_error: false }
+
+ expect(subject).to have_body_text "23, 34, 58"
+ end
+ end
+end
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
new file mode 100644
index 00000000000..83f29718eda
--- /dev/null
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ErrorTracking::ProjectErrorTrackingSetting do
+ set(:project) { create(:project) }
+
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ subject { create(:project_error_tracking_setting, project: project) }
+
+ context 'when api_url is over 255 chars' do
+ before do
+ subject.api_url = 'https://' + 'a' * 250
+ end
+
+ it 'fails validation' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:api_url]).to include('is too long (maximum is 255 characters)')
+ end
+ end
+
+ context 'With unsafe url' do
+ let(:project_error_tracking_setting) { create(:project_error_tracking_setting, project: project) }
+
+ it 'fails validation' do
+ project_error_tracking_setting.api_url = "https://replaceme.com/'><script>alert(document.cookie)</script>"
+
+ expect(project_error_tracking_setting).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index bcdfe3cf1eb..385b8a7959f 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -890,4 +890,19 @@ describe Note do
end
end
end
+
+ describe '#parent' do
+ it 'returns project for project notes' do
+ project = create(:project)
+ note = create(:note_on_issue, project: project)
+
+ expect(note.parent).to eq(project)
+ end
+
+ it 'returns nil for personal snippet note' do
+ note = create(:note_on_personal_snippet)
+
+ expect(note.parent).to be_nil
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 65b59c7b21b..d1ab0bdba29 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -62,6 +62,7 @@ describe Project do
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:fork_network_member) }
it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
+ it { is_expected.to have_one(:error_tracking_setting).class_name('ErrorTracking::ProjectErrorTrackingSetting') }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:ci_pipelines) }
it { is_expected.to have_many(:builds) }
@@ -2410,6 +2411,20 @@ describe Project do
end
end
+ describe '#set_repository_read_only!' do
+ let(:project) { create(:project) }
+
+ it 'returns true when there is no existing git transfer in progress' do
+ expect(project.set_repository_read_only!).to be_truthy
+ end
+
+ it 'returns false when there is an existing git transfer in progress' do
+ allow(project).to receive(:git_transfer_in_progress?) { true }
+
+ expect(project.set_repository_read_only!).to be_falsey
+ end
+ end
+
describe '#pushes_since_gc' do
let(:project) { create(:project) }
@@ -3143,6 +3158,33 @@ describe Project do
end
end
+ describe '#git_transfer_in_progress?' do
+ let(:project) { build(:project) }
+
+ subject { project.git_transfer_in_progress? }
+
+ it 'returns false when repo_reference_count and wiki_reference_count are 0' do
+ allow(project).to receive(:repo_reference_count) { 0 }
+ allow(project).to receive(:wiki_reference_count) { 0 }
+
+ expect(subject).to be_falsey
+ end
+
+ it 'returns true when repo_reference_count is > 0' do
+ allow(project).to receive(:repo_reference_count) { 2 }
+ allow(project).to receive(:wiki_reference_count) { 0 }
+
+ expect(subject).to be_truthy
+ end
+
+ it 'returns true when wiki_reference_count is > 0' do
+ allow(project).to receive(:repo_reference_count) { 0 }
+ allow(project).to receive(:wiki_reference_count) { 2 }
+
+ expect(subject).to be_truthy
+ end
+ end
+
context 'legacy storage' do
let(:project) { create(:project, :repository, :legacy_storage) }
let(:gitlab_shell) { Gitlab::Shell.new }
@@ -3203,10 +3245,6 @@ describe Project do
expect(project.migrate_to_hashed_storage!).to be_truthy
end
- it 'flags as read-only' do
- expect { project.migrate_to_hashed_storage! }.to change { project.repository_read_only }.to(true)
- end
-
it 'does not validate project visibility' do
expect(project).not_to receive(:visibility_level_allowed_as_fork)
expect(project).not_to receive(:visibility_level_allowed_by_group)
diff --git a/spec/services/issues/import_csv_service_spec.rb b/spec/services/issues/import_csv_service_spec.rb
new file mode 100644
index 00000000000..516a1137319
--- /dev/null
+++ b/spec/services/issues/import_csv_service_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Issues::ImportCsvService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ subject do
+ uploader = FileUploader.new(project)
+ uploader.store!(file)
+
+ described_class.new(user, project, uploader).execute
+ end
+
+ describe '#execute' do
+ context 'invalid file' do
+ let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
+
+ it 'returns invalid file error' do
+ expect_any_instance_of(Notify).to receive(:import_issues_csv_email)
+
+ expect(subject[:success]).to eq(0)
+ expect(subject[:parse_error]).to eq(true)
+ end
+ end
+
+ context 'comma delimited file' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }
+
+ it 'imports CSV without errors' do
+ expect_any_instance_of(Notify).to receive(:import_issues_csv_email)
+
+ expect(subject[:success]).to eq(3)
+ expect(subject[:error_lines]).to eq([])
+ expect(subject[:parse_error]).to eq(false)
+ end
+ end
+
+ context 'tab delimited file with error row' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') }
+
+ it 'imports CSV with some error rows' do
+ expect_any_instance_of(Notify).to receive(:import_issues_csv_email)
+
+ expect(subject[:success]).to eq(2)
+ expect(subject[:error_lines]).to eq([3])
+ expect(subject[:parse_error]).to eq(false)
+ end
+ end
+
+ context 'semicolon delimited file with CRLF' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') }
+
+ it 'imports CSV with a blank row' do
+ expect_any_instance_of(Notify).to receive(:import_issues_csv_email)
+
+ expect(subject[:success]).to eq(3)
+ expect(subject[:error_lines]).to eq([4])
+ expect(subject[:parse_error]).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 0e82194e9ee..b720f37ffdb 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -15,6 +15,20 @@ describe Projects::HashedStorage::MigrateRepositoryService do
allow(service).to receive(:gitlab_shell) { gitlab_shell }
end
+ context 'repository lock' do
+ it 'tries to lock the repository' do
+ expect(service).to receive(:try_to_set_repository_read_only!)
+
+ service.execute
+ end
+
+ it 'fails when a git operation is in progress' do
+ allow(project).to receive(:repo_reference_count) { 1 }
+
+ expect { service.execute }.to raise_error(Projects::HashedStorage::RepositoryMigrationError)
+ end
+ end
+
context 'when succeeds' do
it 'renames project and wiki repositories' do
service.execute
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 332c1600cde..731be907453 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -11,6 +11,67 @@ describe Projects::Operations::UpdateService do
subject { described_class.new(project, user, params) }
describe '#execute' do
+ context 'error tracking' do
+ context 'with existing error tracking setting' do
+ let(:params) do
+ {
+ error_tracking_setting_attributes: {
+ enabled: false,
+ api_url: 'http://url',
+ token: 'token'
+ }
+ }
+ end
+
+ before do
+ create(:project_error_tracking_setting, project: project)
+ end
+
+ it 'updates the settings' do
+ expect(result[:status]).to eq(:success)
+
+ project.reload
+ expect(project.error_tracking_setting).not_to be_enabled
+ expect(project.error_tracking_setting.api_url).to eq('http://url')
+ expect(project.error_tracking_setting.token).to eq('token')
+ end
+ end
+
+ context 'without an existing error tracking setting' do
+ let(:params) do
+ {
+ error_tracking_setting_attributes: {
+ enabled: true,
+ api_url: 'http://url',
+ token: 'token'
+ }
+ }
+ end
+
+ it 'creates a setting' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.error_tracking_setting).to be_enabled
+ expect(project.error_tracking_setting.api_url).to eq('http://url')
+ expect(project.error_tracking_setting.token).to eq('token')
+ end
+ end
+
+ context 'with invalid parameters' do
+ let(:params) { {} }
+
+ let!(:error_tracking_setting) do
+ create(:project_error_tracking_setting, project: project)
+ end
+
+ it 'does nothing' do
+ expect(result[:status]).to eq(:success)
+ expect(project.reload.error_tracking_setting)
+ .to eq(error_tracking_setting)
+ end
+ end
+ end
+
context 'with inappropriate params' do
let(:params) { { name: '' } }
diff --git a/spec/services/upload_service_spec.rb b/spec/services/upload_service_spec.rb
index 9b232a52efa..4a809d5bf18 100644
--- a/spec/services/upload_service_spec.rb
+++ b/spec/services/upload_service_spec.rb
@@ -63,11 +63,11 @@ describe UploadService do
@link_to_file = upload_file(@project, txt)
end
- it { expect(@link_to_file).to eq(nil) }
+ it { expect(@link_to_file).to eq({}) }
end
end
def upload_file(project, file)
- described_class.new(project, file, FileUploader).execute
+ described_class.new(project, file, FileUploader).execute.to_h
end
end
diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb
index f3f3386382f..1bb42382e8a 100644
--- a/spec/validators/url_validator_spec.rb
+++ b/spec/validators/url_validator_spec.rb
@@ -172,4 +172,55 @@ describe UrlValidator do
end
end
end
+
+ context 'when enforce_sanitization is' do
+ let(:validator) { described_class.new(attributes: [:link_url], enforce_sanitization: enforce_sanitization) }
+ let(:unsafe_url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" }
+ let(:safe_url) { 'https://replaceme.com/path/to/somewhere' }
+
+ let(:unsafe_internal_url) do
+ Gitlab.config.gitlab.protocol + '://' + Gitlab.config.gitlab.host +
+ "/'><script>alert(document.cookie)</script>"
+ end
+
+ context 'true' do
+ let(:enforce_sanitization) { true }
+
+ it 'prevents unsafe urls' do
+ badge.link_url = unsafe_url
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+
+ it 'prevents unsafe internal urls' do
+ badge.link_url = unsafe_internal_url
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+
+ it 'allows safe urls' do
+ badge.link_url = safe_url
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+ end
+
+ context 'false' do
+ let(:enforce_sanitization) { false }
+
+ it 'allows unsafe urls' do
+ badge.link_url = unsafe_url
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+ end
+ end
end
diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb
new file mode 100644
index 00000000000..752fd82c5e8
--- /dev/null
+++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rails_helper'
+
+describe 'projects/settings/operations/show' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ assign :project, project
+ end
+
+ describe 'Operations > Error Tracking' do
+ before do
+ stub_feature_flags(error_tracking: true)
+
+ project.add_reporter(user)
+
+ allow(view).to receive(:error_tracking_setting)
+ .and_return(error_tracking_setting)
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ let!(:error_tracking_setting) do
+ create(:project_error_tracking_setting, project: project)
+ end
+
+ context 'Settings page ' do
+ it 'renders the Operations Settings page' do
+ render
+
+ expect(rendered).to have_content _('Error Tracking')
+ expect(rendered).to have_content _('To link Sentry to GitLab, enter your Sentry URL and Auth Token')
+ expect(rendered).to have_content _('Active')
+ end
+ end
+ end
+end
diff --git a/spec/workers/import_issues_csv_worker_spec.rb b/spec/workers/import_issues_csv_worker_spec.rb
new file mode 100644
index 00000000000..89370c4890d
--- /dev/null
+++ b/spec/workers/import_issues_csv_worker_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ImportIssuesCsvWorker do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:upload) { create(:upload) }
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'calls #execute on Issues::ImportCsvService and destroys upload' do
+ expect_any_instance_of(Issues::ImportCsvService).to receive(:execute).and_return({ success: 5, errors: [], valid_file: true })
+
+ worker.perform(user.id, project.id, upload.id)
+
+ expect { upload.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 1068d4a68ef..fb1854265b5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -641,10 +641,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.47.0.tgz#c03dda41aefd3889cbfed95a391836106ae2ac4d"
integrity sha512-0Bx/HxqR8xpqqaLnZiFAHIh1jTAFQPFToVZ6Wi3QyhsAwmXRAbgw1SlkRMZ7w3e6l+G71Wnw+GnI4rx1gK8JLQ==
-"@gitlab/ui@^1.18.0":
- version "1.18.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.18.0.tgz#5cc591b2c7958e59fa7b1b443d4235e0e8f956c9"
- integrity sha512-JqmiRSGYmK0DbGBQJBpjeRrcgjK25rCqG6QW6/GPTVLtRjbPPZYGvVg5PyA6nJUGAnwFoeApUZVML6X3OpnV1Q==
+"@gitlab/ui@^1.20.0":
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.20.0.tgz#50bd4b092646a2c6337f0f462779af8e702dda05"
+ integrity sha512-EJgrqon/tYCUPoOgnNNAXbrDXOEAajJwKHr4aR2R6vkJI3kVZiq66RNIe5ftGIUoNqYCDnRIkpLyo7MqzJPgcw==
dependencies:
babel-standalone "^6.26.0"
bootstrap-vue "^2.0.0-rc.11"