diff options
57 files changed, 655 insertions, 175 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index bfbadb3a2ac..53cc1a6f929 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.23.0
\ No newline at end of file +1.24.0 @@ -421,7 +421,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.12.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.13.0', require: 'gitaly' gem 'grpc', '~> 1.15.0' diff --git a/Gemfile.lock b/Gemfile.lock index e4791a98f2f..4d37075cdfa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -279,7 +279,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.12.0) + gitaly-proto (1.13.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -310,7 +310,7 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) google-protobuf (3.6.1) - googleapis-common-protos-types (1.0.2) + googleapis-common-protos-types (1.0.3) google-protobuf (~> 3.0) googleauth (0.6.6) faraday (~> 0.12) @@ -1018,7 +1018,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.12.0) + gitaly-proto (~> 1.13.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-markup (~> 1.6.5) diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 9e031b03579..17e4f325b08 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -57,7 +57,7 @@ export default { }, width: 0, height: 0, - scatterSymbol: undefined, + svgs: {}, }; }, computed: { @@ -78,25 +78,25 @@ export default { axisPointer: { snap: true, }, - nameTextStyle: { - padding: [18, 0, 0, 0], - }, }, yAxis: { name: this.yAxisLabel, axisLabel: { formatter: value => value.toFixed(3), }, - nameTextStyle: { - padding: [0, 0, 36, 0], - }, }, legend: { formatter: this.xAxisLabel, }, series: this.scatterSeries, + dataZoom: this.dataZoomConfig, }; }, + dataZoomConfig() { + const handleIcon = this.svgs['scroll-handle']; + + return handleIcon ? { handleIcon } : {}; + }, earliestDatapoint() { return Object.values(this.chartData).reduce((acc, data) => { const [[timestamp]] = data.sort(([a], [b]) => { @@ -131,7 +131,7 @@ export default { return { type: 'scatter', data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]), - symbol: this.scatterSymbol, + symbol: this.svgs.rocket, symbolSize: 14, }; }, @@ -151,7 +151,8 @@ export default { created() { debouncedResize = debounceByAnimationFrame(this.onResize); window.addEventListener('resize', debouncedResize); - this.getScatterSymbol(); + this.setSvg('rocket'); + this.setSvg('scroll-handle'); }, methods: { formatTooltipText(params) { @@ -167,11 +168,11 @@ export default { this.tooltip.content = `${this.yAxisLabel} ${seriesData.value[1].toFixed(3)}`; } }, - getScatterSymbol() { - getSvgIconPathContent('rocket') + setSvg(name) { + getSvgIconPathContent(name) .then(path => { if (path) { - this.scatterSymbol = `path://${path}`; + this.$set(this.svgs, name, `path://${path}`); } }) .catch(() => {}); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index afa099d0e0b..61204c37307 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -10,6 +10,12 @@ import { __ } from '~/locale'; const d3 = { select, scaleLinear, scaleThreshold }; +const firstDayOfWeekChoices = Object.freeze({ + sunday: 0, + monday: 1, + saturday: 6, +}); + const LOADING_HTML = ` <div class="text-center"> <i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i> @@ -49,7 +55,7 @@ export default class ActivityCalendar { timestamps, calendarActivitiesPath, utcOffset = 0, - firstDayOfWeek = 0, + firstDayOfWeek = firstDayOfWeekChoices.sunday, monthsAgo = 12, ) { this.calendarActivitiesPath = calendarActivitiesPath; @@ -206,11 +212,16 @@ export default class ActivityCalendar { }, ]; - if (this.firstDayOfWeek === 1) { + if (this.firstDayOfWeek === firstDayOfWeekChoices.monday) { days.push({ text: 'S', y: 29 + this.dayYPos(7), }); + } else if (this.firstDayOfWeek === firstDayOfWeekChoices.saturday) { + days.push({ + text: 'S', + y: 29 + this.dayYPos(6), + }); } this.svg diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js index 1e34e74a152..4a08e158f6b 100644 --- a/app/assets/javascripts/persistent_user_callout.js +++ b/app/assets/javascripts/persistent_user_callout.js @@ -31,4 +31,12 @@ export default class PersistentUserCallout { Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); }); } + + static factory(container) { + if (!container) { + return undefined; + } + + return new PersistentUserCallout(container); + } } diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index eed529f93db..766508b6609 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -46,7 +46,8 @@ module PreferencesHelper def first_day_of_week_choices [ [_('Sunday'), 0], - [_('Monday'), 1] + [_('Monday'), 1], + [_('Saturday'), 6] ] end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index daadf9427ba..c5035797621 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -7,7 +7,7 @@ class ApplicationSetting < ActiveRecord::Base include IgnorableColumn include ChronicDurationAttribute - add_authentication_token_field :runners_registration_token, encrypted: true, fallback: true + add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required } add_authentication_token_field :health_check_access_token DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3bfde8d0a77..f39441a1e5b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -138,7 +138,7 @@ module Ci acts_as_taggable - add_authentication_token_field :token, encrypted: true, fallback: true + add_authentication_token_field :token, encrypted: :optional before_save :update_artifacts_size, if: :artifacts_file_changed? before_save :ensure_token diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index d82e11bbb89..ce26ee168ef 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -10,7 +10,7 @@ module Ci include FromUnion include TokenAuthenticatable - add_authentication_token_field :token, encrypted: true, migrating: true + add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption) ? :optional : :required } enum access_level: { not_protected: 0, diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index 01fb194281a..df14e6e4754 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -39,22 +39,6 @@ module TokenAuthenticatableStrategies instance.save! if Gitlab::Database.read_write? end - def fallback? - unless options[:fallback].in?([true, false, nil]) - raise ArgumentError, 'fallback: needs to be a boolean value!' - end - - options[:fallback] == true - end - - def migrating? - unless options[:migrating].in?([true, false, nil]) - raise ArgumentError, 'migrating: needs to be a boolean value!' - end - - options[:migrating] == true - end - def self.fabricate(model, field, options) if options[:digest] && options[:encrypted] raise ArgumentError, 'Incompatible options set!' diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb index 152491aa6e9..2c7fa2c5b3c 100644 --- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb +++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb @@ -2,28 +2,18 @@ module TokenAuthenticatableStrategies class Encrypted < Base - def initialize(*) - super - - if migrating? && fallback? - raise ArgumentError, '`fallback` and `migrating` options are not compatible!' - end - end - def find_token_authenticatable(token, unscoped = false) return if token.blank? - if fully_encrypted? - return find_by_encrypted_token(token, unscoped) - end - - if fallback? + if required? + find_by_encrypted_token(token, unscoped) + elsif optional? find_by_encrypted_token(token, unscoped) || find_by_plaintext_token(token, unscoped) elsif migrating? find_by_plaintext_token(token, unscoped) else - raise ArgumentError, 'Unknown encryption phase!' + raise ArgumentError, "Unknown encryption strategy: #{encrypted_strategy}!" end end @@ -41,8 +31,8 @@ module TokenAuthenticatableStrategies return super if instance.has_attribute?(encrypted_field) - if fully_encrypted? - raise ArgumentError, 'Using encrypted strategy when encrypted field is missing!' + if required? + raise ArgumentError, 'Using required encryption strategy when encrypted field is missing!' else insecure_strategy.ensure_token(instance) end @@ -53,8 +43,7 @@ module TokenAuthenticatableStrategies encrypted_token = instance.read_attribute(encrypted_field) token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) - - token || (insecure_strategy.get_token(instance) if fallback?) + token || (insecure_strategy.get_token(instance) if optional?) end def set_token(instance, token) @@ -62,16 +51,35 @@ module TokenAuthenticatableStrategies instance[encrypted_field] = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) instance[token_field] = token if migrating? - instance[token_field] = nil if fallback? + instance[token_field] = nil if optional? token end - def fully_encrypted? - !migrating? && !fallback? + def required? + encrypted_strategy == :required + end + + def migrating? + encrypted_strategy == :migrating + end + + def optional? + encrypted_strategy == :optional end protected + def encrypted_strategy + value = options[:encrypted] + value = value.call if value.is_a?(Proc) + + unless value.in?([:required, :optional, :migrating]) + raise ArgumentError, 'encrypted: needs to be a :required, :optional or :migrating!' + end + + value + end + def find_by_plaintext_token(token, unscoped) insecure_strategy.find_token_authenticatable(token, unscoped) end @@ -89,7 +97,7 @@ module TokenAuthenticatableStrategies def token_set?(instance) raw_token = instance.read_attribute(encrypted_field) - unless fully_encrypted? + unless required? raw_token ||= insecure_strategy.get_token(instance) end diff --git a/app/models/group.rb b/app/models/group.rb index 52f503404af..495bfe04499 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -56,7 +56,7 @@ class Group < Namespace validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } - add_authentication_token_field :runners_token, encrypted: true, migrating: true + add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption) ? :optional : :required } after_create :post_create_hook after_destroy :post_destroy_hook diff --git a/app/models/project.rb b/app/models/project.rb index 00592c108db..6db88c146b4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -85,7 +85,7 @@ class Project < ActiveRecord::Base default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :only_allow_merge_if_all_discussions_are_resolved, false - add_authentication_token_field :runners_token, encrypted: true, migrating: true + add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required } before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index d075440b147..597431be65a 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -18,13 +18,23 @@ class ProtectedBranch < ActiveRecord::Base def self.protected?(project, ref_name) return true if project.empty_repo? && default_branch_protected? - refs = project.protected_branches.select(:name) + self.matching(ref_name, protected_refs: protected_refs(project)).present? + end - self.matching(ref_name, protected_refs: refs).present? + def self.any_protected?(project, ref_names) + protected_refs(project).any? do |protected_ref| + ref_names.any? do |ref_name| + protected_ref.matches?(ref_name) + end + end end def self.default_branch_protected? Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE end + + def self.protected_refs(project) + project.protected_branches.select(:name) + end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 87749ecf6c0..cf257ed47c8 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -278,6 +278,7 @@ class ProjectPolicy < BasePolicy enable :admin_cluster enable :create_environment_terminal enable :destroy_release + enable :destroy_artifacts enable :daily_statistics end diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index a3b87c20761..bb34a3d3352 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -11,6 +11,7 @@ module Commits @start_project = params[:start_project] || @project @start_branch = params[:start_branch] @branch_name = params[:branch_name] + @force = params[:force] || false end def execute @@ -42,6 +43,10 @@ module Commits @start_branch != @branch_name || @start_project != @project end + def force? + !!@force + end + def validate! validate_permissions! validate_on_branch! @@ -65,13 +70,13 @@ module Commits end def validate_branch_existence! - if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name) + if !project.empty_repo? && different_branch? && repository.branch_exists?(@branch_name) && !force? raise_error("A branch called '#{@branch_name}' already exists. Switch to that branch in order to make changes") end end def validate_new_branch_name! - result = ValidateNewBranchService.new(project, current_user).execute(@branch_name) + result = ValidateNewBranchService.new(project, current_user).execute(@branch_name, force: force?) if result[:status] == :error raise_error("Something went wrong when we tried to create '#{@branch_name}' for you: #{result[:message]}") diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 927634c2159..c1bc26c330a 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -46,7 +46,8 @@ module Files author_email: @author_email, author_name: @author_name, start_project: @start_project, - start_branch_name: @start_branch + start_branch_name: @start_branch, + force: force? ) rescue ArgumentError => e raise_error(e) diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb index c19e2ec2043..3f4a59e5cee 100644 --- a/app/services/validate_new_branch_service.rb +++ b/app/services/validate_new_branch_service.rb @@ -3,14 +3,14 @@ require_relative 'base_service' class ValidateNewBranchService < BaseService - def execute(branch_name) + def execute(branch_name, force: false) valid_branch = Gitlab::GitRefValidator.validate(branch_name) unless valid_branch return error('Branch name is invalid') end - if project.repository.branch_exists?(branch_name) + if project.repository.branch_exists?(branch_name) && !force return error('Branch already exists') end diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index 539b184e5c2..4997770321e 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -12,7 +12,7 @@ %p By default, protected branches are designed to: %ul - %li prevent their creation, if not already created, from everybody except Maintainers + %li prevent their creation, if not already created, from everybody except users who are allowed to merge %li prevent pushes from everybody except Maintainers %li prevent <strong>anyone</strong> from force pushing to the branch %li prevent <strong>anyone</strong> from deleting the branch diff --git a/changelogs/unreleased/45035-force-push-api.yml b/changelogs/unreleased/45035-force-push-api.yml new file mode 100644 index 00000000000..05f5a36ac38 --- /dev/null +++ b/changelogs/unreleased/45035-force-push-api.yml @@ -0,0 +1,5 @@ +--- +title: Accept force option to overwrite branch on commit via API +merge_request: 25286 +author: +type: added diff --git a/changelogs/unreleased/53361-fresh-protected-branches.yml b/changelogs/unreleased/53361-fresh-protected-branches.yml new file mode 100644 index 00000000000..55080e719b7 --- /dev/null +++ b/changelogs/unreleased/53361-fresh-protected-branches.yml @@ -0,0 +1,5 @@ +--- +title: Allow creation of branches that match a wildcard protection, except directly through git +merge_request: 24969 +author: +type: added diff --git a/changelogs/unreleased/57085-introduce-zoom-and-scroll-functionality-on-metrics-charts.yml b/changelogs/unreleased/57085-introduce-zoom-and-scroll-functionality-on-metrics-charts.yml new file mode 100644 index 00000000000..1d07666dfb1 --- /dev/null +++ b/changelogs/unreleased/57085-introduce-zoom-and-scroll-functionality-on-metrics-charts.yml @@ -0,0 +1,5 @@ +--- +title: Add zoom and scroll to metrics dashboard +merge_request: 25388 +author: +type: added diff --git a/changelogs/unreleased/58023-add-Saturday-to-localization-first-day-of-the-week.yml b/changelogs/unreleased/58023-add-Saturday-to-localization-first-day-of-the-week.yml new file mode 100644 index 00000000000..69d927dc5e4 --- /dev/null +++ b/changelogs/unreleased/58023-add-Saturday-to-localization-first-day-of-the-week.yml @@ -0,0 +1,5 @@ +--- +title: Add Saturday to Localization first day of the week +merge_request: 25509 +author: Ahmad Haghighi +type: added diff --git a/changelogs/unreleased/feature-api-delete-job-artifacts.yml b/changelogs/unreleased/feature-api-delete-job-artifacts.yml new file mode 100644 index 00000000000..ddbbe3c2650 --- /dev/null +++ b/changelogs/unreleased/feature-api-delete-job-artifacts.yml @@ -0,0 +1,5 @@ +--- +title: Extend the Gitlab API for deletion of job_artifacts of a single job. +merge_request: 25522 +author: rroger +type: added diff --git a/changelogs/unreleased/use-encrypted-runner-tokens.yml b/changelogs/unreleased/use-encrypted-runner-tokens.yml new file mode 100644 index 00000000000..e01978557bf --- /dev/null +++ b/changelogs/unreleased/use-encrypted-runner-tokens.yml @@ -0,0 +1,5 @@ +--- +title: Use encrypted runner tokens +merge_request: 25532 +author: +type: security diff --git a/db/migrate/20190225160300_steal_encrypt_runners_tokens.rb b/db/migrate/20190225160300_steal_encrypt_runners_tokens.rb new file mode 100644 index 00000000000..18c0d2a2e1b --- /dev/null +++ b/db/migrate/20190225160300_steal_encrypt_runners_tokens.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class StealEncryptRunnersTokens < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + # This cleans after `EncryptRunnersTokens` + + DOWNTIME = false + + disable_ddl_transaction! + + def up + Gitlab::BackgroundMigration.steal('EncryptRunnersTokens') + end + + def down + # no-op + end +end diff --git a/db/migrate/20190225160301_add_runner_tokens_indexes.rb b/db/migrate/20190225160301_add_runner_tokens_indexes.rb new file mode 100644 index 00000000000..3230c2809de --- /dev/null +++ b/db/migrate/20190225160301_add_runner_tokens_indexes.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AddRunnerTokensIndexes < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + # It seems that `ci_runners.token_encrypted` and `projects.runners_token_encrypted` + # are non-unique + + def up + add_concurrent_index :ci_runners, :token_encrypted + add_concurrent_index :projects, :runners_token_encrypted + add_concurrent_index :namespaces, :runners_token_encrypted, unique: true + end + + def down + remove_concurrent_index :ci_runners, :token_encrypted + remove_concurrent_index :projects, :runners_token_encrypted + remove_concurrent_index :namespaces, :runners_token_encrypted, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 2ddc8358433..c782524c391 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -556,6 +556,7 @@ ActiveRecord::Schema.define(version: 20190301081611) do t.index ["locked"], name: "index_ci_runners_on_locked", using: :btree t.index ["runner_type"], name: "index_ci_runners_on_runner_type", using: :btree t.index ["token"], name: "index_ci_runners_on_token", using: :btree + t.index ["token_encrypted"], name: "index_ci_runners_on_token_encrypted", using: :btree end create_table "ci_stages", force: :cascade do |t| @@ -1383,6 +1384,7 @@ ActiveRecord::Schema.define(version: 20190301081611) do t.index ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} t.index ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree t.index ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree + t.index ["runners_token_encrypted"], name: "index_namespaces_on_runners_token_encrypted", unique: true, using: :btree t.index ["type"], name: "index_namespaces_on_type", using: :btree end @@ -1752,6 +1754,7 @@ ActiveRecord::Schema.define(version: 20190301081611) do t.index ["repository_storage", "created_at"], name: "idx_project_repository_check_partial", where: "(last_repository_check_at IS NULL)", using: :btree t.index ["repository_storage"], name: "index_projects_on_repository_storage", using: :btree t.index ["runners_token"], name: "index_projects_on_runners_token", using: :btree + t.index ["runners_token_encrypted"], name: "index_projects_on_runners_token_encrypted", using: :btree t.index ["star_count"], name: "index_projects_on_star_count", using: :btree t.index ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree end diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 9dfe085425f..8c0c7a36736 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -53,7 +53,7 @@ _The uploads are stored by default in > **Notes:** > > - [Introduced][ee-3867] in [GitLab Premium][eep] 10.5. -> - [Introduced][ce17358] in [GitLab Core][ce] 10.7. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17358) in [GitLab Core][ce] 10.7. > - Since version 11.1, we support direct_upload to S3. If you don't want to use the local disk where GitLab is installed to store the @@ -152,4 +152,3 @@ _The uploads are stored by default in [eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Premium" [ce]: https://about.gitlab.com/gitlab-ce/ "GitLab Community Edition" [ee-3867]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3867 -[ce-17358]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17358 diff --git a/doc/api/commits.md b/doc/api/commits.md index 8d36ae7d559..442178aedff 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -79,6 +79,7 @@ POST /projects/:id/repository/commits | `author_email` | string | no | Specify the commit author's email address | | `author_name` | string | no | Specify the commit author's name | | `stats` | boolean | no | Include commit stats. Default is true | +| `force` | boolean | no | When `true` overwrites the target branch with a new commit based on the `start_branch` | | `actions[]` Attribute | Type | Required | Description | | --------------------- | ---- | -------- | ----------- | diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 085e321b35f..877cd99723a 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -10,7 +10,7 @@ GET /projects/:id/jobs | Attribute | Type | Required | Description | |-----------|--------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | | `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. | ```sh @@ -142,8 +142,8 @@ GET /projects/:id/pipelines/:pipeline_id/jobs | Attribute | Type | Required | Description | |---------------|--------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `pipeline_id` | integer | yes | The ID of a pipeline. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `pipeline_id` | integer | yes | ID of a pipeline. | | `scope` | string **or** array of strings | no | Scope of jobs to show. Either one of or an array of the following: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, or `manual`. All jobs are returned if `scope` is not provided. | ```sh @@ -275,8 +275,8 @@ GET /projects/:id/jobs/:job_id | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The ID of a job. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | ```sh curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/8" @@ -350,8 +350,8 @@ GET /projects/:id/jobs/:job_id/artifacts | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The ID of a job. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | Example requests: @@ -385,7 +385,7 @@ Parameters | Attribute | Type | Required | Description | |------------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | | `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. | | `job` | string | yes | The name of the job. | @@ -420,7 +420,7 @@ Parameters | Attribute | Type | Required | Description | |-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | | `job_id ` | integer | yes | The unique job identifier. | | `artifact_path` | string | yes | Path to a file inside the artifacts archive. | @@ -454,7 +454,7 @@ Parameters: | Attribute | Type | Required | Description | |-----------------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | | `ref_name` | string | yes | Branch or tag name in repository. HEAD or SHA references are not supported. | | `artifact_path` | string | yes | Path to a file inside the artifacts archive. | | `job` | string | yes | The name of the job. | @@ -483,8 +483,8 @@ GET /projects/:id/jobs/:job_id/trace | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| id | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| job_id | integer | yes | The ID of a job. | +| id | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| job_id | integer | yes | ID of a job. | ```sh curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/8/trace" @@ -507,8 +507,8 @@ POST /projects/:id/jobs/:job_id/cancel | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The ID of a job. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | ```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/cancel" @@ -555,8 +555,8 @@ POST /projects/:id/jobs/:job_id/retry | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The ID of a job. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | ```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/retry" @@ -605,8 +605,8 @@ Parameters | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The ID of a job. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | Example of request @@ -658,8 +658,8 @@ Parameters | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The ID of a job. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | Example request: @@ -699,6 +699,33 @@ Example response: } ``` +## Delete artifacts + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/25522) in GitLab 11.9. + +Delete artifacts of a job. + +``` +DELETE /projects/:id/jobs/:job_id/artifacts +``` + +| Attribute | Type | Required | Description | +|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `job_id` | integer | yes | ID of a job. | + + +Example request: + +```sh +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/artifacts" +``` + +NOTE: **Note:** +At least Maintainer role is required to delete artifacts. + +If the artifacts were deleted successfully, a response with status `204 No Content` is returned. + ## Play a job Triggers a manual action to start a job. @@ -709,8 +736,8 @@ POST /projects/:id/jobs/:job_id/play | Attribute | Type | Required | Description | |-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `job_id` | integer | yes | The ID of a job. | +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `job_id` | integer | yes | ID of a job. | ```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/1/play" diff --git a/doc/api/settings.md b/doc/api/settings.md index 2e0a2a09133..c2a1f7feefd 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -161,7 +161,7 @@ are listed in the descriptions of the relevant settings. | `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | | `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. | -| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday and `1` for Monday. | +| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. | | `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. | | `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. | | `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. | diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 08db89124de..6c9831dacfd 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -93,7 +93,7 @@ future GitLab releases.** | **CI_MERGE_REQUEST_TARGET_BRANCH_NAME** | 11.6 | all | The target branch name of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | | **CI_MERGE_REQUEST_TARGET_BRANCH_SHA** | 11.9 | all | The HEAD sha of the target branch of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | | **CI_MERGE_REQUEST_TITLE** | 11.9 | all | The title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | -| **CI_MERGE_REQUEST_ASSIGNEES** | 11.9 | all | Comma-separated usernames of the assignees of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). **Comming soon**: [Multitle assignees for merge requests](https://gitlab.com/gitlab-org/gitlab-ee/issues/2004) | +| **CI_MERGE_REQUEST_ASSIGNEES** | 11.9 | all | Comma-separated list of usernames of assignees for the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). [Multiple assignees for merge requests](https://gitlab.com/gitlab-org/gitlab-ee/issues/2004) is scheduled for a future release | | **CI_MERGE_REQUEST_MILESTONE** | 11.9 | all | The milestone title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | | **CI_MERGE_REQUEST_LABELS** | 11.9 | all | Comma-separated label names of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) | | **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. | diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index db68510c46d..f399dc40164 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -103,4 +103,10 @@ Select your preferred language from a list of supported languages. The first day of the week can be customised for calendar views and date pickers. -You can choose **Sunday** or **Monday** as the first day of the week. If you select **System Default**, the system-wide default setting will be used. +You can choose one of the following options as the first day of the week: + +- Saturday +- Sunday +- Monday + +If you select **System Default**, the system-wide default setting will be used. diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index db706e5020e..3eb8123144f 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -10,7 +10,7 @@ created protected branches. By default, a protected branch does four simple things: - it prevents its creation, if not already created, from everybody except users - with Maintainer permission + who are allowed to merge - it prevents pushes from everybody except users with Maintainer permission - it prevents **anyone** from force pushing to the branch - it prevents **anyone** from deleting the branch @@ -94,6 +94,25 @@ all matching branches: ![Protected branch matches](img/protected_branches_matches.png) +## Creating a protected branch + +> [Introduced][https://gitlab.com/gitlab-org/gitlab-ce/issues/53361] in GitLab 11.9. + +When a protected branch or wildcard protected branches are set to +[**No one** is **Allowed to push**](#using-the-allowed-to-merge-and-allowed-to-push-settings), +Developers (and users with higher [permission levels](../permissions.md)) are allowed +to create a new protected branch, but only via the UI or through the API (to avoid +creating protected branches accidentally from the command line or from a Git +client application). + +To create a new branch through the user interface: + +1. Visit **Repository > Branches**. +1. Click on **New branch**. +1. Fill in the branch name and select an existing branch, tag, or commit that + the new branch will be based off. Only existing protected branches and commits + that are already in protected branches will be accepted. + ## Deleting a protected branch > [Introduced][ce-21393] in GitLab 9.3. @@ -125,6 +144,10 @@ for details about the pipelines security model. ## Changelog +**11.9** + +- [Allow protected branches to be created](https://gitlab.com/gitlab-org/gitlab-ce/issues/53361) by Developers (and users with higher permission levels) through the API and the user interface. + **9.2** - Allow deletion of protected branches via the web interface [gitlab-org/gitlab-ce#21393][ce-21393] diff --git a/lib/api/commits.rb b/lib/api/commits.rb index d0a9debda5b..65eb9bfb87e 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -99,6 +99,7 @@ module API optional :author_email, type: String, desc: 'Author email for commit' optional :author_name, type: String, desc: 'Author name for commit' optional :stats, type: Boolean, default: true, desc: 'Include commit stats' + optional :force, type: Boolean, default: false, desc: 'When `true` overwrites the target branch with a new commit based on the `start_branch`' end post ':id/repository/commits' do authorize_push_to_branch!(params[:branch]) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 54cd4cd9cdb..825fab62034 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -244,6 +244,10 @@ module API authorize! :read_build, user_project end + def authorize_destroy_artifacts! + authorize! :destroy_artifacts, user_project + end + def authorize_update_builds! authorize! :update_build, user_project end diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 933bd067e26..e7fed55170e 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -109,6 +109,22 @@ module API status 200 present build, with: Entities::Job end + + desc 'Delete the artifacts files from a job' do + detail 'This feature was introduced in GitLab 11.9' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + delete ':id/jobs/:job_id/artifacts' do + authorize_destroy_artifacts! + build = find_build!(params[:job_id]) + authorize!(:destroy_artifacts, build) + + build.erase_erasable_artifacts! + + status :no_content + end end end end diff --git a/lib/gitlab/background_migration/encrypt_columns.rb b/lib/gitlab/background_migration/encrypt_columns.rb index b9ad8267e37..173543b7c25 100644 --- a/lib/gitlab/background_migration/encrypt_columns.rb +++ b/lib/gitlab/background_migration/encrypt_columns.rb @@ -91,7 +91,8 @@ module Gitlab # No need to do anything if the plaintext is nil, or an encrypted # value already exists - return nil unless plaintext.present? && !ciphertext.present? + return unless plaintext.present? + return if ciphertext.present? # attr_encrypted will calculate and set the expected value for us instance.public_send("#{plain_column}=", plaintext) # rubocop:disable GitlabSecurity/PublicSend diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb index d06b2df36f2..bd305ace0a0 100644 --- a/lib/gitlab/checks/branch_check.rb +++ b/lib/gitlab/checks/branch_check.rb @@ -9,13 +9,17 @@ module Gitlab non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.', non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.', merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.', - push_protected_branch: 'You are not allowed to push code to protected branches on this project.' + push_protected_branch: 'You are not allowed to push code to protected branches on this project.', + create_protected_branch: 'You are not allowed to create protected branches on this project.', + invalid_commit_create_protected_branch: 'You can only use an existing protected branch ref as the basis of a new protected branch.', + non_web_create_protected_branch: 'You can only create protected branches using the web interface and API.' }.freeze LOG_MESSAGES = { delete_default_branch_check: "Checking if default branch is being deleted...", protected_branch_checks: "Checking if you are force pushing to a protected branch...", protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...", + protected_branch_creation_checks: "Checking if you are allowed to create a protected branch...", protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch..." }.freeze @@ -42,13 +46,31 @@ module Gitlab end end - if deletion? + if creation? && protected_branch_creation_enabled? + protected_branch_creation_checks + elsif deletion? protected_branch_deletion_checks else protected_branch_push_checks end end + def protected_branch_creation_checks + logger.log_timed(LOG_MESSAGES[:protected_branch_creation_checks]) do + unless user_access.can_merge_to_branch?(branch_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_branch] + end + + unless safe_commit_for_new_protected_branch? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:invalid_commit_create_protected_branch] + end + + unless updated_from_web? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_create_protected_branch] + end + end + end + def protected_branch_deletion_checks logger.log_timed(LOG_MESSAGES[:protected_branch_deletion_checks]) do unless user_access.can_delete_branch?(branch_name) @@ -98,6 +120,10 @@ module Gitlab Gitlab::Routing.url_helpers.project_project_members_url(project) end + def protected_branch_creation_enabled? + Feature.enabled?(:protected_branch_creation, project, default_enabled: true) + end + def matching_merge_request? Checks::MatchingMergeRequest.new(newrev, branch_name, project).match? end @@ -105,6 +131,10 @@ module Gitlab def forced_push? Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev) end + + def safe_commit_for_new_protected_branch? + ProtectedBranch.any_protected?(project, project.repository.branch_names_contains_sha(newrev)) + end end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 2bfff8397e8..2f8e4e9e93d 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -853,17 +853,20 @@ module Gitlab true end + # rubocop:disable Metrics/ParameterLists def multi_action( user, branch_name:, message:, actions:, author_email: nil, author_name: nil, - start_branch_name: nil, start_repository: self) + start_branch_name: nil, start_repository: self, + force: false) wrapped_gitaly_errors do gitaly_operation_client.user_commit_files(user, branch_name, message, actions, author_email, author_name, - start_branch_name, start_repository) + start_branch_name, start_repository, force) end end + # rubocop:enable Metrics/ParameterLists def write_config(full_path:) return unless full_path.present? diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index d172c798da2..bc45ee38fb5 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -277,14 +277,14 @@ module Gitlab end end + # rubocop:disable Metrics/ParameterLists def user_commit_files( user, branch_name, commit_message, actions, author_email, author_name, - start_branch_name, start_repository) - + start_branch_name, start_repository, force = false) req_enum = Enumerator.new do |y| header = user_commit_files_request_header(user, branch_name, commit_message, actions, author_email, author_name, - start_branch_name, start_repository) + start_branch_name, start_repository, force) y.yield Gitaly::UserCommitFilesRequest.new(header: header) @@ -319,6 +319,7 @@ module Gitlab Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update) end + # rubocop:enable Metrics/ParameterLists def user_commit_patches(user, branch_name, patches) header = Gitaly::UserApplyPatchRequest::Header.new( @@ -382,9 +383,10 @@ module Gitlab Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update) end + # rubocop:disable Metrics/ParameterLists def user_commit_files_request_header( user, branch_name, commit_message, actions, author_email, author_name, - start_branch_name, start_repository) + start_branch_name, start_repository, force) Gitaly::UserCommitFilesRequestHeader.new( repository: @gitaly_repo, @@ -394,9 +396,11 @@ module Gitlab commit_author_name: encode_binary(author_name), commit_author_email: encode_binary(author_email), start_branch_name: encode_binary(start_branch_name), - start_repository: start_repository.gitaly_repository + start_repository: start_repository.gitaly_repository, + force: force ) end + # rubocop:enable Metrics/ParameterLists def user_commit_files_action_header(action) Gitaly::UserCommitFilesActionHeader.new( diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 980a8014409..9ef23cf849f 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -118,8 +118,8 @@ module Gitlab protected_refs: project.protected_tags) end - request_cache def protected?(kind, project, ref) - kind.protected?(project, ref) + request_cache def protected?(kind, project, refs) + kind.protected?(project, refs) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 92784f2c49d..8d2799a1aab 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6515,6 +6515,9 @@ msgstr "" msgid "SSL Verification" msgstr "" +msgid "Saturday" +msgstr "" + msgid "Save" msgstr "" diff --git a/package.json b/package.json index 533750e1b52..578e6873495 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", "@gitlab/svgs": "^1.54.0", - "@gitlab/ui": "^2.1.1", + "@gitlab/ui": "^2.2.0", "apollo-boost": "^0.3.1", "apollo-client": "^2.5.1", "autosize": "^4.0.0", diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index e0e8ebd0c3c..db0d45c3692 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -36,10 +36,11 @@ describe PreferencesHelper do end describe '#first_day_of_week_choices' do - it 'returns Sunday and Monday as choices' do + it 'returns Saturday, Sunday and Monday as choices' do expect(helper.first_day_of_week_choices).to eq [ ['Sunday', 0], - ['Monday', 1] + ['Monday', 1], + ['Saturday', 6] ] end end @@ -47,14 +48,21 @@ describe PreferencesHelper do describe '#first_day_of_week_choices_with_default' do it 'returns choices including system default' do expect(helper.first_day_of_week_choices_with_default).to eq [ - ['System default (Sunday)', nil], ['Sunday', 0], ['Monday', 1] + ['System default (Sunday)', nil], ['Sunday', 0], ['Monday', 1], ['Saturday', 6] ] end it 'returns choices including system default set to Monday' do stub_application_setting(first_day_of_week: 1) expect(helper.first_day_of_week_choices_with_default).to eq [ - ['System default (Monday)', nil], ['Sunday', 0], ['Monday', 1] + ['System default (Monday)', nil], ['Sunday', 0], ['Monday', 1], ['Saturday', 6] + ] + end + + it 'returns choices including system default set to Saturday' do + stub_application_setting(first_day_of_week: 6) + expect(helper.first_day_of_week_choices_with_default).to eq [ + ['System default (Saturday)', nil], ['Sunday', 0], ['Monday', 1], ['Saturday', 6] ] end end diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index d334ef7ba4f..1b6fc456ceb 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -7,6 +7,7 @@ import MonitoringMock, { deploymentData } from '../mock_data'; describe('Area component', () => { const mockWidgets = 'mockWidgets'; + const mockSvgPathContent = 'mockSvgPathContent'; let mockGraphData; let areaChart; let spriteSpy; @@ -30,7 +31,7 @@ describe('Area component', () => { }); spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake( - () => new Promise(resolve => resolve()), + () => new Promise(resolve => resolve(mockSvgPathContent)), ); }); @@ -146,13 +147,22 @@ describe('Area component', () => { }); }); - describe('getScatterSymbol', () => { + describe('setSvg', () => { + const mockSvgName = 'mockSvgName'; + beforeEach(() => { - areaChart.vm.getScatterSymbol(); + areaChart.vm.setSvg(mockSvgName); + }); + + it('gets svg path content', () => { + expect(spriteSpy).toHaveBeenCalledWith(mockSvgName); }); - it('gets rocket svg path content for use as deployment data symbol', () => { - expect(spriteSpy).toHaveBeenCalledWith('rocket'); + it('sets svg path content', done => { + areaChart.vm.$nextTick(() => { + expect(areaChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`); + done(); + }); }); }); diff --git a/spec/javascripts/persistent_user_callout_spec.js b/spec/javascripts/persistent_user_callout_spec.js new file mode 100644 index 00000000000..2fdfff3db03 --- /dev/null +++ b/spec/javascripts/persistent_user_callout_spec.js @@ -0,0 +1,88 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import PersistentUserCallout from '~/persistent_user_callout'; +import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; + +describe('PersistentUserCallout', () => { + const dismissEndpoint = '/dismiss'; + const featureName = 'feature'; + + function createFixture() { + const fixture = document.createElement('div'); + fixture.innerHTML = ` + <div + class="container" + data-dismiss-endpoint="${dismissEndpoint}" + data-feature-id="${featureName}" + > + <button type="button" class="js-close"></button> + </div> + `; + + return fixture; + } + + describe('dismiss', () => { + let button; + let mockAxios; + let persistentUserCallout; + + beforeEach(() => { + const fixture = createFixture(); + const container = fixture.querySelector('.container'); + button = fixture.querySelector('.js-close'); + mockAxios = new MockAdapter(axios); + persistentUserCallout = new PersistentUserCallout(container); + spyOn(persistentUserCallout.container, 'remove'); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('POSTs endpoint and removes container when clicking close', done => { + mockAxios.onPost(dismissEndpoint).replyOnce(200); + + button.click(); + + setTimeoutPromise() + .then(() => { + expect(persistentUserCallout.container.remove).toHaveBeenCalled(); + expect(mockAxios.history.post[0].data).toBe( + JSON.stringify({ feature_name: featureName }), + ); + }) + .then(done) + .catch(done.fail); + }); + + it('invokes Flash when the dismiss request fails', done => { + const Flash = spyOnDependency(PersistentUserCallout, 'Flash'); + mockAxios.onPost(dismissEndpoint).replyOnce(500); + + button.click(); + + setTimeoutPromise() + .then(() => { + expect(persistentUserCallout.container.remove).not.toHaveBeenCalled(); + expect(Flash).toHaveBeenCalledWith( + 'An error occurred while dismissing the alert. Refresh the page and try again.', + ); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('factory', () => { + it('returns an instance of PersistentUserCallout with the provided container property', () => { + const fixture = createFixture(); + + expect(PersistentUserCallout.factory(fixture) instanceof PersistentUserCallout).toBe(true); + }); + + it('returns undefined if container is falsey', () => { + expect(PersistentUserCallout.factory()).toBe(undefined); + }); + }); +}); diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index 77366e91dca..f99fc639dbd 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -55,6 +55,106 @@ describe Gitlab::Checks::BranchCheck do end end + context 'branch creation' do + let(:oldrev) { '0000000000000000000000000000000000000000' } + let(:ref) { 'refs/heads/feature' } + + context 'protected branch creation feature is disabled' do + before do + stub_feature_flags(protected_branch_creation: false) + end + + context 'user is not allowed to push to protected branch' do + before do + allow(user_access) + .to receive(:can_push_to_branch?) + .and_return(false) + end + + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.') + end + end + + context 'user is allowed to push to protected branch' do + before do + allow(user_access) + .to receive(:can_push_to_branch?) + .and_return(true) + end + + it 'does not raise an error' do + expect { subject.validate! }.not_to raise_error + end + end + end + + context 'protected branch creation feature is enabled' do + context 'user is not allowed to create protected branches' do + before do + allow(user_access) + .to receive(:can_merge_to_branch?) + .with('feature') + .and_return(false) + end + + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.') + end + end + + context 'user is allowed to create protected branches' do + before do + allow(user_access) + .to receive(:can_merge_to_branch?) + .with('feature') + .and_return(true) + + allow(project.repository) + .to receive(:branch_names_contains_sha) + .with(newrev) + .and_return(['branch']) + end + + context "newrev isn't in any protected branches" do + before do + allow(ProtectedBranch) + .to receive(:any_protected?) + .with(project, ['branch']) + .and_return(false) + end + + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.') + end + end + + context 'newrev is included in a protected branch' do + before do + allow(ProtectedBranch) + .to receive(:any_protected?) + .with(project, ['branch']) + .and_return(true) + end + + context 'via web interface' do + let(:protocol) { 'web' } + + it 'allows branch creation' do + expect { subject.validate! }.not_to raise_error + end + end + + context 'via SSH' do + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.') + end + end + end + end + end + end + context 'branch deletion' do let(:newrev) { '0000000000000000000000000000000000000000' } let(:ref) { 'refs/heads/feature' } diff --git a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb index 6605f1f5a5f..2a0182b4294 100644 --- a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb @@ -15,7 +15,7 @@ describe TokenAuthenticatableStrategies::Base do context 'when encrypted strategy is specified' do it 'fabricates encrypted strategy object' do - strategy = described_class.fabricate(instance, field, encrypted: true) + strategy = described_class.fabricate(instance, field, encrypted: :required) expect(strategy).to be_a TokenAuthenticatableStrategies::Encrypted end @@ -23,7 +23,7 @@ describe TokenAuthenticatableStrategies::Base do context 'when no strategy is specified' do it 'fabricates insecure strategy object' do - strategy = described_class.fabricate(instance, field, something: true) + strategy = described_class.fabricate(instance, field, something: :required) expect(strategy).to be_a TokenAuthenticatableStrategies::Insecure end @@ -31,35 +31,9 @@ describe TokenAuthenticatableStrategies::Base do context 'when incompatible options are provided' do it 'raises an error' do - expect { described_class.fabricate(instance, field, digest: true, encrypted: true) } + expect { described_class.fabricate(instance, field, digest: true, encrypted: :required) } .to raise_error ArgumentError end end end - - describe '#fallback?' do - context 'when fallback is set' do - it 'recognizes fallback setting' do - strategy = described_class.new(instance, field, fallback: true) - - expect(strategy.fallback?).to be true - end - end - - context 'when fallback is not a valid value' do - it 'raises an error' do - strategy = described_class.new(instance, field, fallback: 'something') - - expect { strategy.fallback? }.to raise_error ArgumentError - end - end - - context 'when fallback is not set' do - it 'raises an error' do - strategy = described_class.new(instance, field, {}) - - expect(strategy.fallback?).to eq false - end - end - end end diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb index 93cab80cb1f..ca38f86c5ab 100644 --- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb @@ -12,19 +12,9 @@ describe TokenAuthenticatableStrategies::Encrypted do described_class.new(model, 'some_field', options) end - describe '.new' do - context 'when fallback and migration strategies are set' do - let(:options) { { fallback: true, migrating: true } } - - it 'raises an error' do - expect { subject }.to raise_error ArgumentError, /not compatible/ - end - end - end - describe '#find_token_authenticatable' do - context 'when using fallback strategy' do - let(:options) { { fallback: true } } + context 'when using optional strategy' do + let(:options) { { encrypted: :optional } } it 'finds the encrypted resource by cleartext' do allow(model).to receive(:find_by) @@ -50,7 +40,7 @@ describe TokenAuthenticatableStrategies::Encrypted do end context 'when using migration strategy' do - let(:options) { { migrating: true } } + let(:options) { { encrypted: :migrating } } it 'finds the cleartext resource by cleartext' do allow(model).to receive(:find_by) @@ -73,8 +63,8 @@ describe TokenAuthenticatableStrategies::Encrypted do end describe '#get_token' do - context 'when using fallback strategy' do - let(:options) { { fallback: true } } + context 'when using optional strategy' do + let(:options) { { encrypted: :optional } } it 'returns decrypted token when an encrypted token is present' do allow(instance).to receive(:read_attribute) @@ -98,7 +88,7 @@ describe TokenAuthenticatableStrategies::Encrypted do end context 'when using migration strategy' do - let(:options) { { migrating: true } } + let(:options) { { encrypted: :migrating } } it 'returns cleartext token when an encrypted token is present' do allow(instance).to receive(:read_attribute) @@ -127,8 +117,8 @@ describe TokenAuthenticatableStrategies::Encrypted do end describe '#set_token' do - context 'when using fallback strategy' do - let(:options) { { fallback: true } } + context 'when using optional strategy' do + let(:options) { { encrypted: :optional } } it 'writes encrypted token and removes plaintext token and returns it' do expect(instance).to receive(:[]=) @@ -141,7 +131,7 @@ describe TokenAuthenticatableStrategies::Encrypted do end context 'when using migration strategy' do - let(:options) { { migrating: true } } + let(:options) { { encrypted: :migrating } } it 'writes encrypted token and writes plaintext token' do expect(instance).to receive(:[]=) diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index 4c677200ae2..dafe7646366 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -190,4 +190,32 @@ describe ProtectedBranch do end end end + + describe '#any_protected?' do + context 'existing project' do + let(:project) { create(:project, :repository) } + + it 'returns true when any of the branch names match a protected branch via direct match' do + create(:protected_branch, project: project, name: 'foo') + + expect(described_class.any_protected?(project, ['foo', 'production/some-branch'])).to eq(true) + end + + it 'returns true when any of the branch matches a protected branch via wildcard match' do + create(:protected_branch, project: project, name: 'production/*') + + expect(described_class.any_protected?(project, ['foo', 'production/some-branch'])).to eq(true) + end + + it 'returns false when none of branches does not match a protected branch via direct match' do + expect(described_class.any_protected?(project, ['foo'])).to eq(false) + end + + it 'returns false when none of the branches does not match a protected branch via wildcard match' do + create(:protected_branch, project: project, name: 'production/*') + + expect(described_class.any_protected?(project, ['staging/some-branch'])).to eq(false) + end + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 47491f708e9..772d1fbee2b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -45,8 +45,7 @@ describe ProjectPolicy do let(:base_maintainer_permissions) do %i[ push_to_delete_protected_branch update_project_snippet update_environment - update_deployment admin_project_snippet - admin_project_member admin_note admin_wiki admin_project + update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project admin_commit_status admin_build admin_container_image admin_pipeline admin_environment admin_deployment destroy_release add_cluster daily_statistics diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 3defe8bbf51..ed2ef4c730b 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -321,6 +321,49 @@ describe API::Jobs do end end + describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do + let!(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) } + + before do + delete api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) + end + + context 'when user is anonymous' do + let(:api_user) { nil } + + it 'does not delete artifacts' do + expect(job.job_artifacts.size).to eq 2 + end + + it 'returns status 401 (unauthorized)' do + expect(response).to have_http_status :unauthorized + end + end + + context 'with developer' do + it 'does not delete artifacts' do + expect(job.job_artifacts.size).to eq 2 + end + + it 'returns status 403 (forbidden)' do + expect(response).to have_http_status :forbidden + end + end + + context 'with authorized user' do + let(:maintainer) { create(:project_member, :maintainer, project: project).user } + let!(:api_user) { maintainer } + + it 'deletes artifacts' do + expect(job.job_artifacts.size).to eq 0 + end + + it 'returns status 204 (no content)' do + expect(response).to have_http_status :no_content + end + end + end + describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do context 'when job has artifacts' do let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb index 84c48d63c64..6842fa9f435 100644 --- a/spec/services/files/multi_service_spec.rb +++ b/spec/services/files/multi_service_spec.rb @@ -235,6 +235,22 @@ describe Files::MultiService do expect(blob).to be_present end end + + context 'when force is set to true and branch already exists' do + let(:commit_params) do + { + commit_message: commit_message, + branch_name: 'feature', + start_branch: 'master', + actions: actions, + force: true + } + end + + it 'is still a success' do + expect(subject.execute[:status]).to eq(:success) + end + end end def update_file(path) diff --git a/yarn.lock b/yarn.lock index f8d27049c3a..45767b9512f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -663,10 +663,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.54.0.tgz#00320e845efd46716042cde0c348b990d4908daf" integrity sha512-DR17iy8TM5IbXEacqiDP0p8SuC/J8EL+98xbfVz5BKvRsPHpeZJQNlBF/petIV5d+KWM5A9v3GZTY7uMU7z/JQ== -"@gitlab/ui@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.1.1.tgz#e869c0573c0dcd273257fef553feb8e3a056305e" - integrity sha512-hK1UIDPXdLlREqXn1Y6VfUSc68R4ZeWOYTEOhd+9Dfnp9RPe8tCWdnWY4nf3U8n1X3EaYgtzI9ho39qnF26nAA== +"@gitlab/ui@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.2.0.tgz#8e384d3fb3d84f2886eacea75feb05e0ea42adcc" + integrity sha512-CCr1CjFyeycm1vrTtRKng5VknWWTN3fFw48YQThz/rgg0viVtA12oqz7oqGGAC+AktnWXtA/cxkXjVNpKTmEpA== dependencies: "@babel/standalone" "^7.0.0" bootstrap-vue "^2.0.0-rc.11" |