summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile9
-rw-r--r--Gemfile.lock25
-rw-r--r--app/assets/javascripts/notes.js12
-rw-r--r--app/models/concerns/atomic_internal_id.rb46
-rw-r--r--app/models/concerns/nonatomic_internal_id.rb (renamed from app/models/concerns/internal_id.rb)2
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/internal_id.rb125
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/project.rb2
-rw-r--r--app/services/projects/destroy_service.rb6
-rw-r--r--app/views/projects/diffs/_stats.html.haml4
-rw-r--r--changelogs/unreleased/31114-internal-ids-are-not-atomic.yml5
-rw-r--r--changelogs/unreleased/44022-singular-1-diff.yml5
-rw-r--r--changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml5
-rw-r--r--changelogs/unreleased/sh-fix-failure-project-destroy.yml5
-rw-r--r--config/initializers/ar_native_database_types.rb11
-rw-r--r--db/migrate/20180305095250_create_internal_ids_table.rb15
-rw-r--r--db/schema.rb9
-rw-r--r--doc/install/kubernetes/index.md2
-rw-r--r--lib/gitlab/diff/file.rb39
-rw-r--r--package.json2
-rw-r--r--spec/factories/internal_ids.rb7
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/internal_id_spec.rb106
-rw-r--r--spec/models/issue_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_spec.rb40
-rw-r--r--spec/views/projects/diffs/_stats.html.haml_spec.rb56
-rw-r--r--yarn.lock6
33 files changed, 517 insertions, 62 deletions
diff --git a/Gemfile b/Gemfile
index dcd5f5ee049..bbb97cd696b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,8 +22,8 @@ gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
-gem 'doorkeeper', '~> 4.2.0'
-gem 'doorkeeper-openid_connect', '~> 1.2.0'
+gem 'doorkeeper', '~> 4.3'
+gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
@@ -34,7 +34,7 @@ gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.2'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
-gem 'omniauth-saml', '~> 1.10.0'
+gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
@@ -235,9 +235,6 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
-# Faster JSON
-gem 'oj', '~> 2.17.4'
-
# Faster blank
gem 'fast_blank'
diff --git a/Gemfile.lock b/Gemfile.lock
index 618fbf4b3e7..817772dfdcb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -47,6 +47,7 @@ GEM
memoizable (~> 0.4.0)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
+ aes_key_wrap (1.0.1)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
@@ -86,7 +87,7 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
- bindata (2.4.1)
+ bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
@@ -176,10 +177,10 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (4.2.6)
+ doorkeeper (4.3.1)
railties (>= 4.2)
- doorkeeper-openid_connect (1.2.0)
- doorkeeper (~> 4.0)
+ doorkeeper-openid_connect (1.3.0)
+ doorkeeper (~> 4.3)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
@@ -428,10 +429,10 @@ GEM
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
json (1.8.6)
- json-jwt (1.7.2)
+ json-jwt (1.9.2)
activesupport
+ aes_key_wrap
bindata
- multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.8.0)
@@ -523,7 +524,6 @@ GEM
rack (>= 1.2, < 3)
octokit (4.8.0)
sawyer (~> 0.8.0, >= 0.5.3)
- oj (2.17.5)
omniauth (1.4.3)
hashie (>= 1.2, < 4)
rack (>= 1.6.2, < 3)
@@ -677,8 +677,8 @@ GEM
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
- rails-dom-testing (1.0.8)
- activesupport (>= 4.2.0.beta, < 5.0)
+ rails-dom-testing (1.0.9)
+ activesupport (>= 4.2.0, < 5.0)
nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
@@ -1030,8 +1030,8 @@ DEPENDENCIES
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
- doorkeeper (~> 4.2.0)
- doorkeeper-openid_connect (~> 1.2.0)
+ doorkeeper (~> 4.3)
+ doorkeeper-openid_connect (~> 1.3)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
@@ -1106,7 +1106,6 @@ DEPENDENCIES
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
octokit (~> 4.8)
- oj (~> 2.17.4)
omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1)
@@ -1118,7 +1117,7 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 0.5.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
- omniauth-saml (~> 1.10.0)
+ omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 659ae575219..2afa4e4c1bf 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -105,6 +105,9 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
+ this.$wrapperEl = hasVueMRDiscussionsCookie()
+ ? $(document).find('.diffs')
+ : $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
@@ -138,10 +141,6 @@ export default class Notes {
}
addBinding() {
- this.$wrapperEl = hasVueMRDiscussionsCookie()
- ? $(document).find('.diffs')
- : $(document);
-
// Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
@@ -226,14 +225,9 @@ export default class Notes {
$(window).on('hashchange', this.onHashChange);
this.boundGetContent = this.getContent.bind(this);
document.addEventListener('refreshLegacyNotes', this.boundGetContent);
- this.eventsBound = true;
}
cleanBinding() {
- if (!this.eventsBound) {
- return;
- }
-
this.$wrapperEl.off('click', '.js-note-edit');
this.$wrapperEl.off('click', '.note-edit-cancel');
this.$wrapperEl.off('click', '.js-note-delete');
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
new file mode 100644
index 00000000000..4b66725a3e6
--- /dev/null
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -0,0 +1,46 @@
+# Include atomic internal id generation scheme for a model
+#
+# This allows us to atomically generate internal ids that are
+# unique within a given scope.
+#
+# For example, let's generate internal ids for Issue per Project:
+# ```
+# class Issue < ActiveRecord::Base
+# has_internal_id :iid, scope: :project, init: ->(s) { s.project.issues.maximum(:iid) }
+# end
+# ```
+#
+# This generates unique internal ids per project for newly created issues.
+# The generated internal id is saved in the `iid` attribute of `Issue`.
+#
+# This concern uses InternalId records to facilitate atomicity.
+# In the absence of a record for the given scope, one will be created automatically.
+# In this situation, the `init` block is called to calculate the initial value.
+# In the example above, we calculate the maximum `iid` of all issues
+# within the given project.
+#
+# Note that a model may have more than one internal id associated with possibly
+# different scopes.
+module AtomicInternalId
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
+ before_validation(on: :create) do
+ if read_attribute(column).blank?
+ scope_attrs = { scope => association(scope).reader }
+ usage = self.class.table_name.to_sym
+
+ new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
+ write_attribute(column, new_iid)
+ end
+ end
+
+ validates column, presence: true, numericality: true
+ end
+ end
+
+ def to_param
+ iid.to_s
+ end
+end
diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/nonatomic_internal_id.rb
index 01079fb8bd6..9d0c9b8512f 100644
--- a/app/models/concerns/internal_id.rb
+++ b/app/models/concerns/nonatomic_internal_id.rb
@@ -1,4 +1,4 @@
-module InternalId
+module NonatomicInternalId
extend ActiveSupport::Concern
included do
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 66e61c06765..e18ea8bfea4 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -1,5 +1,5 @@
class Deployment < ActiveRecord::Base
- include InternalId
+ include NonatomicInternalId
belongs_to :project, required: true
belongs_to :environment, required: true
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
new file mode 100644
index 00000000000..cbec735c2dd
--- /dev/null
+++ b/app/models/internal_id.rb
@@ -0,0 +1,125 @@
+# An InternalId is a strictly monotone sequence of integers
+# generated for a given scope and usage.
+#
+# For example, issues use their project to scope internal ids:
+# In that sense, scope is "project" and usage is "issues".
+# Generated internal ids for an issue are unique per project.
+#
+# See InternalId#usage enum for available usages.
+#
+# In order to leverage InternalId for other usages, the idea is to
+# * Add `usage` value to enum
+# * (Optionally) add columns to `internal_ids` if needed for scope.
+class InternalId < ActiveRecord::Base
+ belongs_to :project
+
+ enum usage: { issues: 0 }
+
+ validates :usage, presence: true
+
+ REQUIRED_SCHEMA_VERSION = 20180305095250
+
+ # Increments #last_value and saves the record
+ #
+ # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
+ # As such, the increment is atomic and safe to be called concurrently.
+ def increment_and_save!
+ lock!
+ self.last_value = (last_value || 0) + 1
+ save!
+ last_value
+ end
+
+ class << self
+ def generate_next(subject, scope, usage, init)
+ # Shortcut if `internal_ids` table is not available (yet)
+ # This can be the case in other (unrelated) migration specs
+ return (init.call(subject) || 0) + 1 unless available?
+
+ InternalIdGenerator.new(subject, scope, usage, init).generate
+ end
+
+ def available?
+ @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
+ end
+
+ # Flushes cached information about schema
+ def reset_column_information
+ @available_flag = nil
+ super
+ end
+ end
+
+ class InternalIdGenerator
+ # Generate next internal id for a given scope and usage.
+ #
+ # For currently supported usages, see #usage enum.
+ #
+ # The method implements a locking scheme that has the following properties:
+ # 1) Generated sequence of internal ids is unique per (scope and usage)
+ # 2) The method is thread-safe and may be used in concurrent threads/processes.
+ # 3) The generated sequence is gapless.
+ # 4) In the absence of a record in the internal_ids table, one will be created
+ # and last_value will be calculated on the fly.
+ #
+ # subject: The instance we're generating an internal id for. Gets passed to init if called.
+ # scope: Attributes that define the scope for id generation.
+ # usage: Symbol to define the usage of the internal id, see InternalId.usages
+ # init: Block that gets called to initialize InternalId record if not present
+ # Make sure to not throw exceptions in the absence of records (if this is expected).
+ attr_reader :subject, :scope, :init, :scope_attrs, :usage
+
+ def initialize(subject, scope, usage, init)
+ @subject = subject
+ @scope = scope
+ @init = init
+ @usage = usage
+
+ raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty?
+
+ unless InternalId.usages.has_key?(usage.to_s)
+ raise ArgumentError, "Usage '#{usage}' is unknown. Supported values are #{InternalId.usages.keys} from InternalId.usages"
+ end
+ end
+
+ # Generates next internal id and returns it
+ def generate
+ subject.transaction do
+ # Create a record in internal_ids if one does not yet exist
+ # and increment its last value
+ #
+ # Note this will acquire a ROW SHARE lock on the InternalId record
+ (lookup || create_record).increment_and_save!
+ end
+ end
+
+ private
+
+ # Retrieve InternalId record for (project, usage) combination, if it exists
+ def lookup
+ InternalId.find_by(**scope, usage: usage_value)
+ end
+
+ def usage_value
+ @usage_value ||= InternalId.usages[usage.to_s]
+ end
+
+ # Create InternalId record for (scope, usage) combination, if it doesn't exist
+ #
+ # We blindly insert without synchronization. If another process
+ # was faster in doing this, we'll realize once we hit the unique key constraint
+ # violation. We can safely roll-back the nested transaction and perform
+ # a lookup instead to retrieve the record.
+ def create_record
+ subject.transaction(requires_new: true) do
+ InternalId.create!(
+ **scope,
+ usage: usage_value,
+ last_value: init.call(subject) || 0
+ )
+ end
+ rescue ActiveRecord::RecordNotUnique
+ lookup
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index c81f7e52bb1..7bfc45c1f43 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -1,7 +1,7 @@
require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
- include InternalId
+ include AtomicInternalId
include Issuable
include Noteable
include Referable
@@ -24,6 +24,8 @@ class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
+
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 149ef7ec429..7e6d89ec9c7 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,5 +1,5 @@
class MergeRequest < ActiveRecord::Base
- include InternalId
+ include NonatomicInternalId
include Issuable
include Noteable
include Referable
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 77c19380e66..e7d397f40f5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
- include InternalId
+ include NonatomicInternalId
include Sortable
include Referable
include StripAttribute
diff --git a/app/models/project.rb b/app/models/project.rb
index d6e663f14e4..e5ede967668 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -188,6 +188,8 @@ class Project < ActiveRecord::Base
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :internal_ids
+
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 81972df9b3c..4b8f955ae69 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -88,7 +88,11 @@ module Projects
def attempt_rollback(project, message)
return unless project
- project.update_attributes(delete_error: message, pending_delete: false)
+ # It's possible that the project was destroyed, but some after_commit
+ # hook failed and caused us to end up here. A destroyed model will be a frozen hash,
+ # which cannot be altered.
+ project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed?
+
log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
end
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index b082ad0ef0e..6fd6018dea3 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -7,9 +7,9 @@
= icon("caret-down", class: "prepend-left-5")
%span.diff-stats-additions-deletions-expanded#diff-stats
with
- %strong.cgreen #{sum_added_lines} additions
+ %strong.cgreen= pluralize(sum_added_lines, 'addition')
and
- %strong.cred #{sum_removed_lines} deletions
+ %strong.cred= pluralize(sum_removed_lines, 'deletion')
.diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
%strong.cgreen<
+#{sum_added_lines}
diff --git a/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml b/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
new file mode 100644
index 00000000000..bc1955bc66f
--- /dev/null
+++ b/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
@@ -0,0 +1,5 @@
+---
+title: Atomic generation of internal ids for issues.
+merge_request: 17580
+author:
+type: other
diff --git a/changelogs/unreleased/44022-singular-1-diff.yml b/changelogs/unreleased/44022-singular-1-diff.yml
new file mode 100644
index 00000000000..f4942925a73
--- /dev/null
+++ b/changelogs/unreleased/44022-singular-1-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Use singular in the diff stats if only one line has been changed
+merge_request: 17697
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml b/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
new file mode 100644
index 00000000000..934860b95fe
--- /dev/null
+++ b/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
@@ -0,0 +1,5 @@
+---
+title: Fix viewing diffs on old merge requests
+merge_request: 17805
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-failure-project-destroy.yml b/changelogs/unreleased/sh-fix-failure-project-destroy.yml
new file mode 100644
index 00000000000..d5f5cd3f954
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-failure-project-destroy.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "Can't modify frozen hash" error when project is destroyed
+merge_request:
+author:
+type: fixed
diff --git a/config/initializers/ar_native_database_types.rb b/config/initializers/ar_native_database_types.rb
new file mode 100644
index 00000000000..3522b1db536
--- /dev/null
+++ b/config/initializers/ar_native_database_types.rb
@@ -0,0 +1,11 @@
+require 'active_record/connection_adapters/abstract_mysql_adapter'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractMysqlAdapter
+ NATIVE_DATABASE_TYPES.merge!(
+ bigserial: { name: 'bigint(20) auto_increment PRIMARY KEY' }
+ )
+ end
+ end
+end
diff --git a/db/migrate/20180305095250_create_internal_ids_table.rb b/db/migrate/20180305095250_create_internal_ids_table.rb
new file mode 100644
index 00000000000..432086fe98b
--- /dev/null
+++ b/db/migrate/20180305095250_create_internal_ids_table.rb
@@ -0,0 +1,15 @@
+class CreateInternalIdsTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :internal_ids, id: :bigserial do |t|
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }
+ t.integer :usage, null: false
+ t.integer :last_value, null: false
+
+ t.index [:usage, :project_id], unique: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ab4370e2754..3ff1a8754e2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -866,6 +866,14 @@ ActiveRecord::Schema.define(version: 20180309160427) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
+ create_table "internal_ids", id: :bigserial, force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "usage", null: false
+ t.integer "last_value", null: false
+ end
+
+ add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, using: :btree
+
create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_id", null: false
@@ -2058,6 +2066,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index cd889e74487..3a1707a54a1 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -37,7 +37,7 @@ By offering individual containers and charts, we will be able to provide a numbe
This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We are planning to launch this chart in beta by the end of 2017.
-Learn more about the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
+Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
## Other Charts
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 34b070dd375..014854da55c 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -27,8 +27,8 @@ module Gitlab
@fallback_diff_refs = fallback_diff_refs
# Ensure items are collected in the the batch
- new_blob
- old_blob
+ new_blob_lazy
+ old_blob_lazy
end
def position(position_marker, position_type: :text)
@@ -101,25 +101,19 @@ module Gitlab
end
def new_blob
- return unless new_content_sha
-
- Blob.lazy(repository.project, new_content_sha, file_path)
+ new_blob_lazy&.itself
end
def old_blob
- return unless old_content_sha
-
- Blob.lazy(repository.project, old_content_sha, old_path)
+ old_blob_lazy&.itself
end
def content_sha
new_content_sha || old_content_sha
end
- # Use #itself to check the value wrapped by a BatchLoader instance, rather
- # than if the BatchLoader instance itself is falsey.
def blob
- new_blob&.itself || old_blob&.itself
+ new_blob || old_blob
end
attr_writer :highlighted_diff_lines
@@ -237,17 +231,14 @@ module Gitlab
private
- # The blob instances are instances of BatchLoader, which means calling
- # &. directly on them won't work. Object#try also won't work, because Blob
- # doesn't inherit from Object, but from BasicObject (via SimpleDelegator).
+ # We can't use Object#try because Blob doesn't inherit from Object, but
+ # from BasicObject (via SimpleDelegator).
def try_blobs(meth)
- old_blob&.itself&.public_send(meth) || new_blob&.itself&.public_send(meth)
+ old_blob&.public_send(meth) || new_blob&.public_send(meth)
end
- # We can't use #compact for the same reason we can't use &., but calling
- # #nil? explicitly does work because it is proxied to the blob itself.
def valid_blobs
- [old_blob, new_blob].reject(&:nil?)
+ [old_blob, new_blob].compact
end
def text_position_properties(line)
@@ -262,6 +253,18 @@ module Gitlab
old_blob && new_blob && old_blob.id != new_blob.id
end
+ def new_blob_lazy
+ return unless new_content_sha
+
+ Blob.lazy(repository.project, new_content_sha, file_path)
+ end
+
+ def old_blob_lazy
+ return unless old_content_sha
+
+ Blob.lazy(repository.project, old_content_sha, old_path)
+ end
+
def simple_viewer_class
return DiffViewer::NotDiffable unless diffable?
diff --git a/package.json b/package.json
index deee668ae3b..c81020f631e 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "^1.14.0",
+ "@gitlab-org/gitlab-svgs": "^1.16.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-core": "^6.26.0",
diff --git a/spec/factories/internal_ids.rb b/spec/factories/internal_ids.rb
new file mode 100644
index 00000000000..fbde07a391a
--- /dev/null
+++ b/spec/factories/internal_ids.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :internal_id do
+ project
+ usage :issues
+ last_value { project.issues.maximum(:iid) || 0 }
+ end
+end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 9204ea37963..0c2e18c268a 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -455,5 +455,17 @@ describe Gitlab::Diff::File do
expect(diff_file.size).to be_zero
end
end
+
+ describe '#different_type?' do
+ it 'returns false' do
+ expect(diff_file).not_to be_different_type
+ end
+ end
+
+ describe '#content_changed?' do
+ it 'returns false' do
+ expect(diff_file).not_to be_content_changed
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index bece82e531a..a204a8f1ffe 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -279,6 +279,7 @@ project:
- lfs_file_locks
- project_badges
- source_of_merge_requests
+- internal_ids
award_emoji:
- awardable
- user
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4b217df2e8f..f8874d14e3f 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -34,7 +34,7 @@ describe Issuable do
subject { build(:issue) }
before do
- allow(subject).to receive(:set_iid).and_return(false)
+ allow(InternalId).to receive(:generate_next).and_return(nil)
end
it { is_expected.to validate_presence_of(:project) }
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
new file mode 100644
index 00000000000..581fd0293cc
--- /dev/null
+++ b/spec/models/internal_id_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe InternalId do
+ let(:project) { create(:project) }
+ let(:usage) { :issues }
+ let(:issue) { build(:issue, project: project) }
+ let(:scope) { { project: project } }
+ let(:init) { ->(s) { s.project.issues.size } }
+
+ context 'validations' do
+ it { is_expected.to validate_presence_of(:usage) }
+ end
+
+ describe '.generate_next' do
+ subject { described_class.generate_next(issue, scope, usage, init) }
+
+ context 'in the absence of a record' do
+ it 'creates a record if not yet present' do
+ expect { subject }.to change { described_class.count }.from(0).to(1)
+ end
+
+ it 'stores record attributes' do
+ subject
+
+ described_class.first.tap do |record|
+ expect(record.project).to eq(project)
+ expect(record.usage).to eq(usage.to_s)
+ end
+ end
+
+ context 'with existing issues' do
+ before do
+ rand(1..10).times { create(:issue, project: project) }
+ described_class.delete_all
+ end
+
+ it 'calculates last_value values automatically' do
+ expect(subject).to eq(project.issues.size + 1)
+ end
+ end
+
+ context 'with concurrent inserts on table' do
+ it 'looks up the record if it was created concurrently' do
+ args = { **scope, usage: described_class.usages[usage.to_s] }
+ record = double
+ expect(described_class).to receive(:find_by).with(args).and_return(nil) # first call, record not present
+ expect(described_class).to receive(:find_by).with(args).and_return(record) # second call, record was created by another process
+ expect(described_class).to receive(:create!).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(record).to receive(:increment_and_save!)
+
+ subject
+ end
+ end
+ end
+
+ it 'generates a strictly monotone, gapless sequence' do
+ seq = (0..rand(100)).map do
+ described_class.generate_next(issue, scope, usage, init)
+ end
+ normalized = seq.map { |i| i - seq.min }
+
+ expect(normalized).to eq((0..seq.size - 1).to_a)
+ end
+
+ context 'with an insufficient schema version' do
+ before do
+ described_class.reset_column_information
+ expect(ActiveRecord::Migrator).to receive(:current_version).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ end
+
+ let(:init) { double('block') }
+
+ it 'calculates next internal ids on the fly' do
+ val = rand(1..100)
+
+ expect(init).to receive(:call).with(issue).and_return(val)
+ expect(subject).to eq(val + 1)
+ end
+ end
+ end
+
+ describe '#increment_and_save!' do
+ let(:id) { create(:internal_id) }
+ subject { id.increment_and_save! }
+
+ it 'returns incremented iid' do
+ value = id.last_value
+
+ expect(subject).to eq(value + 1)
+ end
+
+ it 'saves the record' do
+ subject
+
+ expect(id.changed?).to be_falsey
+ end
+
+ context 'with last_value=nil' do
+ let(:id) { build(:internal_id, last_value: nil) }
+
+ it 'returns 1' do
+ expect(subject).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index feed7968f09..11154291368 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -9,11 +9,17 @@ describe Issue do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(InternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:issue) }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :issues }
+ end
end
subject { create(:issue) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 4e783acbd8b..ff5a6f63010 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -17,7 +17,7 @@ describe MergeRequest do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(InternalId) }
+ it { is_expected.to include_module(NonatomicInternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
new file mode 100644
index 00000000000..144af4fc475
--- /dev/null
+++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+shared_examples_for 'AtomicInternalId' do
+ describe '.has_internal_id' do
+ describe 'Module inclusion' do
+ subject { described_class }
+
+ it { is_expected.to include_module(AtomicInternalId) }
+ end
+
+ describe 'Validation' do
+ subject { instance }
+
+ before do
+ allow(InternalId).to receive(:generate_next).and_return(nil)
+ end
+
+ it { is_expected.to validate_presence_of(internal_id_attribute) }
+ it { is_expected.to validate_numericality_of(internal_id_attribute) }
+ end
+
+ describe 'internal id generation' do
+ subject { instance.save! }
+
+ it 'calls InternalId.generate_next and sets internal id attribute' do
+ iid = rand(1..1000)
+
+ expect(InternalId).to receive(:generate_next).with(instance, scope_attrs, usage, any_args).and_return(iid)
+ subject
+ expect(instance.public_send(internal_id_attribute)).to eq(iid)
+ end
+
+ it 'does not overwrite an existing internal id' do
+ instance.public_send("#{internal_id_attribute}=", 4711)
+
+ expect { subject }.not_to change { instance.public_send(internal_id_attribute) }
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/diffs/_stats.html.haml_spec.rb b/spec/views/projects/diffs/_stats.html.haml_spec.rb
new file mode 100644
index 00000000000..c7d2f85747c
--- /dev/null
+++ b/spec/views/projects/diffs/_stats.html.haml_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'projects/diffs/_stats.html.haml' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+
+ def render_view
+ render partial: "projects/diffs/stats", locals: { diff_files: commit.diffs.diff_files }
+ end
+
+ context 'when the commit contains several changes' do
+ it 'uses plural for additions' do
+ render_view
+
+ expect(rendered).to have_text('additions')
+ end
+
+ it 'uses plural for deletions' do
+ render_view
+ end
+ end
+
+ context 'when the commit contains no addition and no deletions' do
+ let(:commit) { project.commit('4cd80ccab63c82b4bad16faa5193fbd2aa06df40') }
+
+ it 'uses plural for additions' do
+ render_view
+
+ expect(rendered).to have_text('additions')
+ end
+
+ it 'uses plural for deletions' do
+ render_view
+
+ expect(rendered).to have_text('deletions')
+ end
+ end
+
+ context 'when the commit contains exactly one addition and one deletion' do
+ let(:commit) { project.commit('08f22f255f082689c0d7d39d19205085311542bc') }
+
+ it 'uses singular for additions' do
+ render_view
+
+ expect(rendered).to have_text('addition')
+ expect(rendered).not_to have_text('additions')
+ end
+
+ it 'uses singular for deletions' do
+ render_view
+
+ expect(rendered).to have_text('deletion')
+ expect(rendered).not_to have_text('deletions')
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 3cc5445c402..36683a2a480 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,9 +54,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.14.0":
- version "1.14.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.14.0.tgz#b4a5cca3106f33224c5486cf674ba3b70cee727e"
+"@gitlab-org/gitlab-svgs@^1.16.0":
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.16.0.tgz#6c88a1bd9f5b3d3e5bf6a6d89d61724022185667"
"@types/jquery@^2.0.40":
version "2.0.48"