summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorYorick Peterse <yorickpeterse@gmail.com>2017-07-10 17:43:57 +0200
committerYorick Peterse <yorickpeterse@gmail.com>2017-08-10 17:45:44 +0200
commit0395c47193b3bbf6b4f060f28c9f632580313a35 (patch)
treed7cf216392120385186c36d18bd38443ae5b1642 /spec
parentafd6e517e59aa07ab377fc29da45be08918a4f8e (diff)
downloadgitlab-ce-0395c47193b3bbf6b4f060f28c9f632580313a35.tar.gz
Migrate events into a new format
This commit migrates events data in such a way that push events are stored much more efficiently. This is done by creating a shadow table called "events_for_migration", and a table called "push_event_payloads" which is used for storing push data of push events. The background migration in this commit will copy events from the "events" table into the "events_for_migration" table, push events in will also have a row created in "push_event_payloads". This approach allows us to reclaim space in the next release by simply swapping the "events" and "events_for_migration" tables, then dropping the old events (now "events_for_migration") table. The new table structure is also optimised for storage space, and does not include the unused "title" column nor the "data" column (since this data is moved to "push_event_payloads"). == Newly Created Events Newly created events are inserted into both "events" and "events_for_migration", both using the exact same primary key value. The table "push_event_payloads" in turn has a foreign key to the _shadow_ table. This removes the need for recreating and validating the foreign key after swapping the tables. Since the shadow table also has a foreign key to "projects.id" we also don't have to worry about orphaned rows. This approach however does require some additional storage as we're duplicating a portion of the events data for at least 1 release. The exact amount is hard to estimate, but for GitLab.com this is expected to be between 10 and 20 GB at most. The background migration in this commit deliberately does _not_ update the "events" table as doing so would put a lot of pressure on PostgreSQL's auto vacuuming system. == Supporting Both Old And New Events Application code has also been adjusted to support push events using both the old and new data formats. This is done by creating a PushEvent class which extends the regular Event class. Using Rails' Single Table Inheritance system we can ensure the right class is used for the right data, which in this case is based on the value of `events.action`. To support displaying old and new data at the same time the PushEvent class re-defines a few methods of the Event class, falling back to their original implementations for push events in the old format. Once all existing events have been migrated the various push event related methods can be removed from the Event model, and the calls to `super` can be removed from the methods in the PushEvent model. The UI and event atom feed have also been slightly changed to better handle this new setup, fortunately only a few changes were necessary to make this work. == API Changes The API only displays push data of events in the new format. Supporting both formats in the API is a bit more difficult compared to the UI. Since the old push data was not really well documented (apart from one example that used an incorrect "action" nmae) I decided that supporting both was not worth the effort, especially since events will be migrated in a few days _and_ new events are created in the correct format.
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/users_controller_spec.rb10
-rw-r--r--spec/factories/events.rb16
-rw-r--r--spec/features/calendar_spec.rb16
-rw-r--r--spec/features/dashboard/activity_spec.rb28
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb4
-rw-r--r--spec/lib/event_filter_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb423
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml8
-rw-r--r--spec/models/event_spec.rb32
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/project_spec.rb4
-rw-r--r--spec/models/push_event_payload_spec.rb16
-rw-r--r--spec/models/push_event_spec.rb202
-rw-r--r--spec/models/user_spec.rb25
-rw-r--r--spec/requests/api/events_spec.rb28
-rw-r--r--spec/requests/api/v3/users_spec.rb25
-rw-r--r--spec/services/event_create_service_spec.rb44
-rw-r--r--spec/services/git_push_service_spec.rb7
-rw-r--r--spec/services/push_event_payload_service_spec.rb218
-rw-r--r--spec/workers/prune_old_events_worker_spec.rb8
21 files changed, 1052 insertions, 69 deletions
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index a64ad73cba8..2cecd2646fc 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -92,8 +92,14 @@ describe UsersController do
before do
sign_in(user)
project.team << [user, :developer]
- EventCreateService.new.push(project, user, [])
- EventCreateService.new.push(forked_project, user, [])
+
+ push_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+
+ fork_push_data = Gitlab::DataBuilder::Push
+ .build_sample(forked_project, user)
+
+ EventCreateService.new.push(project, user, push_data)
+ EventCreateService.new.push(forked_project, user, fork_push_data)
end
it 'includes forked projects' do
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 11d2016955c..ad9f7e2caef 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -2,6 +2,7 @@ FactoryGirl.define do
factory :event do
project
author factory: :user
+ action Event::JOINED
trait(:created) { action Event::CREATED }
trait(:updated) { action Event::UPDATED }
@@ -20,4 +21,19 @@ FactoryGirl.define do
target factory: :closed_issue
end
end
+
+ factory :push_event, class: PushEvent do
+ project factory: :project_empty_repo
+ author factory: :user
+ action Event::PUSHED
+ end
+
+ factory :push_event_payload do
+ event
+ commit_count 1
+ action :pushed
+ ref_type :branch
+ ref 'master'
+ commit_to '3cdce97ed87c91368561584e7358f4d46e3e173c'
+ end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 64fbc80cb81..9a597a2d690 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -42,14 +42,14 @@ feature 'Contributions Calendar', :js do
end
def push_code_contribution
- push_params = {
- project: contributed_project,
- action: Event::PUSHED,
- author_id: user.id,
- data: { commit_count: 3 }
- }
-
- Event.create(push_params)
+ event = create(:push_event, project: contributed_project, author: user)
+
+ create(:push_event_payload,
+ event: event,
+ commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce',
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 3,
+ ref: 'master')
end
def note_comment_contribution
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index 4917dfcf1d1..582868bac1e 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -23,27 +23,19 @@ feature 'Dashboard > Activity' do
create(:merge_request, author: user, source_project: project, target_project: project)
end
- let(:push_event_data) do
- {
- before: Gitlab::Git::BLANK_SHA,
- after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
- ref: 'refs/heads/new_design',
- user_id: user.id,
- user_name: user.name,
- repository: {
- name: project.name,
- url: 'localhost/rubinius',
- description: '',
- homepage: 'localhost/rubinius',
- private: true
- }
- }
- end
-
let(:note) { create(:note, project: project, noteable: merge_request) }
let!(:push_event) do
- create(:event, :pushed, data: push_event_data, project: project, author: user)
+ event = create(:push_event, project: project, author: user)
+
+ create(:push_event_payload,
+ event: event,
+ action: :created,
+ commit_to: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
+ ref: 'new_design',
+ commit_count: 1)
+
+ event
end
let!(:merged_event) do
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index 2d079ea83b4..60ea98e61c7 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -14,8 +14,8 @@ describe ContributedProjectsFinder do
private_project.add_developer(current_user)
public_project.add_master(source_user)
- create(:event, :pushed, project: public_project, target: public_project, author: source_user)
- create(:event, :pushed, project: private_project, target: private_project, author: source_user)
+ create(:push_event, project: public_project, author: source_user)
+ create(:push_event, project: private_project, author: source_user)
end
describe 'without a current user' do
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index b0efcab47fb..87ae6b6cf01 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -5,7 +5,7 @@ describe EventFilter do
let(:source_user) { create(:user) }
let!(:public_project) { create(:project, :public) }
- let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) }
+ let!(:push_event) { create(:push_event, project: public_project, author: source_user) }
let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) }
let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) }
let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) }
diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
new file mode 100644
index 00000000000..87f45619e7a
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
@@ -0,0 +1,423 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event do
+ describe '#commit_title' do
+ it 'returns nil when there are no commits' do
+ expect(described_class.new.commit_title).to be_nil
+ end
+
+ it 'returns nil when there are commits without commit messages' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ id: '123' }])
+
+ expect(event.commit_title).to be_nil
+ end
+
+ it 'returns the commit message when it is less than 70 characters long' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ message: 'Hello world' }])
+
+ expect(event.commit_title).to eq('Hello world')
+ end
+
+ it 'returns the first line of a commit message if multiple lines are present' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ message: "Hello\n\nworld" }])
+
+ expect(event.commit_title).to eq('Hello')
+ end
+
+ it 'truncates the commit to 70 characters when it is too long' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ message: 'a' * 100 }])
+
+ expect(event.commit_title).to eq(('a' * 67) + '...')
+ end
+ end
+
+ describe '#commit_from_sha' do
+ it 'returns nil when pushing to a new ref' do
+ event = described_class.new
+
+ allow(event).to receive(:create?).and_return(true)
+
+ expect(event.commit_from_sha).to be_nil
+ end
+
+ it 'returns the ID of the first commit when pushing to an existing ref' do
+ event = described_class.new
+
+ allow(event).to receive(:create?).and_return(false)
+ allow(event).to receive(:data).and_return(before: '123')
+
+ expect(event.commit_from_sha).to eq('123')
+ end
+ end
+
+ describe '#commit_to_sha' do
+ it 'returns nil when removing an existing ref' do
+ event = described_class.new
+
+ allow(event).to receive(:remove?).and_return(true)
+
+ expect(event.commit_to_sha).to be_nil
+ end
+
+ it 'returns the ID of the last commit when pushing to an existing ref' do
+ event = described_class.new
+
+ allow(event).to receive(:remove?).and_return(false)
+ allow(event).to receive(:data).and_return(after: '123')
+
+ expect(event.commit_to_sha).to eq('123')
+ end
+ end
+
+ describe '#data' do
+ it 'returns the deserialized data' do
+ event = described_class.new(data: { before: '123' })
+
+ expect(event.data).to eq(before: '123')
+ end
+
+ it 'returns an empty hash when no data is present' do
+ event = described_class.new
+
+ expect(event.data).to eq({})
+ end
+ end
+
+ describe '#commits' do
+ it 'returns an Array of commits' do
+ event = described_class.new(data: { commits: [{ id: '123' }] })
+
+ expect(event.commits).to eq([{ id: '123' }])
+ end
+
+ it 'returns an empty array when no data is present' do
+ event = described_class.new
+
+ expect(event.commits).to eq([])
+ end
+ end
+
+ describe '#commit_count' do
+ it 'returns the number of commits' do
+ event = described_class.new(data: { total_commits_count: 2 })
+
+ expect(event.commit_count).to eq(2)
+ end
+
+ it 'returns 0 when no data is present' do
+ event = described_class.new
+
+ expect(event.commit_count).to eq(0)
+ end
+ end
+
+ describe '#ref' do
+ it 'returns the name of the ref' do
+ event = described_class.new(data: { ref: 'refs/heads/master' })
+
+ expect(event.ref).to eq('refs/heads/master')
+ end
+ end
+
+ describe '#trimmed_ref_name' do
+ it 'returns the trimmed ref name for a branch' do
+ event = described_class.new(data: { ref: 'refs/heads/master' })
+
+ expect(event.trimmed_ref_name).to eq('master')
+ end
+
+ it 'returns the trimmed ref name for a tag' do
+ event = described_class.new(data: { ref: 'refs/tags/v1.2' })
+
+ expect(event.trimmed_ref_name).to eq('v1.2')
+ end
+ end
+
+ describe '#create?' do
+ it 'returns true when creating a new ref' do
+ event = described_class.new(data: { before: described_class::BLANK_REF })
+
+ expect(event.create?).to eq(true)
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ event = described_class.new(data: { before: '123' })
+
+ expect(event.create?).to eq(false)
+ end
+ end
+
+ describe '#remove?' do
+ it 'returns true when removing an existing ref' do
+ event = described_class.new(data: { after: described_class::BLANK_REF })
+
+ expect(event.remove?).to eq(true)
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ event = described_class.new(data: { after: '123' })
+
+ expect(event.remove?).to eq(false)
+ end
+ end
+
+ describe '#push_action' do
+ let(:event) { described_class.new }
+
+ it 'returns :created when creating a new ref' do
+ allow(event).to receive(:create?).and_return(true)
+
+ expect(event.push_action).to eq(:created)
+ end
+
+ it 'returns :removed when removing an existing ref' do
+ allow(event).to receive(:create?).and_return(false)
+ allow(event).to receive(:remove?).and_return(true)
+
+ expect(event.push_action).to eq(:removed)
+ end
+
+ it 'returns :pushed when pushing to an existing ref' do
+ allow(event).to receive(:create?).and_return(false)
+ allow(event).to receive(:remove?).and_return(false)
+
+ expect(event.push_action).to eq(:pushed)
+ end
+ end
+
+ describe '#ref_type' do
+ let(:event) { described_class.new }
+
+ it 'returns :tag for a tag' do
+ allow(event).to receive(:ref).and_return('refs/tags/1.2')
+
+ expect(event.ref_type).to eq(:tag)
+ end
+
+ it 'returns :branch for a branch' do
+ allow(event).to receive(:ref).and_return('refs/heads/1.2')
+
+ expect(event.ref_type).to eq(:branch)
+ end
+ end
+end
+
+describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads do
+ let(:migration) { described_class.new }
+ let(:project) { create(:project_empty_repo) }
+ let(:author) { create(:user) }
+
+ # We can not rely on FactoryGirl as the state of Event may change in ways that
+ # the background migration does not expect, hence we use the Event class of
+ # the migration itself.
+ def create_push_event(project, author, data = nil)
+ klass = Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event
+
+ klass.create!(
+ action: klass::PUSHED,
+ project_id: project.id,
+ author_id: author.id,
+ data: data
+ )
+ end
+
+ # The background migration relies on a temporary table, hence we're migrating
+ # to a specific version of the database where said table is still present.
+ before :all do
+ ActiveRecord::Migration.verbose = false
+
+ ActiveRecord::Migrator
+ .migrate(ActiveRecord::Migrator.migrations_paths, 20170608152748)
+ end
+
+ after :all do
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths)
+
+ ActiveRecord::Migration.verbose = true
+ end
+
+ describe '#perform' do
+ it 'returns if data should not be migrated' do
+ allow(migration).to receive(:migrate?).and_return(false)
+
+ expect(migration).not_to receive(:find_events)
+
+ migration.perform(1, 10)
+ end
+
+ it 'migrates the range of events if data is to be migrated' do
+ event1 = create_push_event(project, author, { commits: [] })
+ event2 = create_push_event(project, author, { commits: [] })
+
+ allow(migration).to receive(:migrate?).and_return(true)
+
+ expect(migration).to receive(:process_event).twice
+
+ migration.perform(event1.id, event2.id)
+ end
+ end
+
+ describe '#process_event' do
+ it 'processes a regular event' do
+ event = double(:event, push_event?: false)
+
+ expect(migration).to receive(:replicate_event)
+ expect(migration).not_to receive(:create_push_event_payload)
+
+ migration.process_event(event)
+ end
+
+ it 'processes a push event' do
+ event = double(:event, push_event?: true)
+
+ expect(migration).to receive(:replicate_event)
+ expect(migration).to receive(:create_push_event_payload)
+
+ migration.process_event(event)
+ end
+ end
+
+ describe '#replicate_event' do
+ it 'replicates the event to the "events_for_migration" table' do
+ event = create_push_event(
+ project,
+ author,
+ data: { commits: [] },
+ title: 'bla'
+ )
+
+ attributes = event
+ .attributes.with_indifferent_access.except(:title, :data)
+
+ expect(described_class::EventForMigration)
+ .to receive(:create!)
+ .with(attributes)
+
+ migration.replicate_event(event)
+ end
+ end
+
+ describe '#create_push_event_payload' do
+ let(:push_data) do
+ {
+ commits: [],
+ ref: 'refs/heads/master',
+ before: '156e0e9adc587a383a7eeb5b21ddecb9044768a8',
+ after: '0' * 40,
+ total_commits_count: 1
+ }
+ end
+
+ let(:event) do
+ create_push_event(project, author, push_data)
+ end
+
+ before do
+ # The foreign key in push_event_payloads at this point points to the
+ # "events_for_migration" table so we need to make sure a row exists in
+ # said table.
+ migration.replicate_event(event)
+ end
+
+ it 'creates a push event payload for an event' do
+ payload = migration.create_push_event_payload(event)
+
+ expect(PushEventPayload.count).to eq(1)
+ expect(payload.valid?).to eq(true)
+ end
+
+ it 'does not create push event payloads for removed events' do
+ allow(event).to receive(:id).and_return(-1)
+
+ payload = migration.create_push_event_payload(event)
+
+ expect(payload).to be_nil
+ expect(PushEventPayload.count).to eq(0)
+ end
+
+ it 'encodes and decodes the commit IDs from and to binary data' do
+ payload = migration.create_push_event_payload(event)
+ packed = migration.pack(push_data[:before])
+
+ expect(payload.commit_from).to eq(packed)
+ expect(payload.commit_to).to be_nil
+ end
+ end
+
+ describe '#find_events' do
+ it 'returns the events for the given ID range' do
+ event1 = create_push_event(project, author, { commits: [] })
+ event2 = create_push_event(project, author, { commits: [] })
+ event3 = create_push_event(project, author, { commits: [] })
+ events = migration.find_events(event1.id, event2.id)
+
+ expect(events.length).to eq(2)
+ expect(events.pluck(:id)).not_to include(event3.id)
+ end
+ end
+
+ describe '#migrate?' do
+ it 'returns true when data should be migrated' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::PushEventPayload)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::EventForMigration)
+ .to receive(:table_exists?).and_return(true)
+
+ expect(migration.migrate?).to eq(true)
+ end
+
+ it 'returns false if the "events" table does not exist' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(false)
+
+ expect(migration.migrate?).to eq(false)
+ end
+
+ it 'returns false if the "push_event_payloads" table does not exist' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::PushEventPayload)
+ .to receive(:table_exists?).and_return(false)
+
+ expect(migration.migrate?).to eq(false)
+ end
+
+ it 'returns false when the "events_for_migration" table does not exist' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::PushEventPayload)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::EventForMigration)
+ .to receive(:table_exists?).and_return(false)
+
+ expect(migration.migrate?).to eq(false)
+ end
+ end
+
+ describe '#pack' do
+ it 'packs a SHA1 into a 20 byte binary string' do
+ packed = migration.pack('156e0e9adc587a383a7eeb5b21ddecb9044768a8')
+
+ expect(packed.bytesize).to eq(20)
+ end
+
+ it 'returns nil if the input value is nil' do
+ expect(migration.pack(nil)).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 6a41afe0c25..8da02b0cf00 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -22,6 +22,7 @@ events:
- author
- project
- target
+- push_event_payload
notes:
- award_emoji
- project
@@ -272,3 +273,5 @@ timelogs:
- issue
- merge_request
- user
+push_event_payload:
+- event
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 4dce48f8079..ae3b0173160 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -36,6 +36,14 @@ Event:
- updated_at
- action
- author_id
+PushEventPayload:
+- commit_count
+- action
+- ref_type
+- commit_from
+- commit_to
+- ref
+- commit_title
Note:
- id
- note
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index d86bf1a90a9..ff3224dd298 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -304,27 +304,15 @@ describe Event do
end
end
- def create_push_event(project, user, attrs = {})
- data = {
- before: Gitlab::Git::BLANK_SHA,
- after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e",
- ref: "refs/heads/master",
- user_id: user.id,
- user_name: user.name,
- repository: {
- name: project.name,
- url: "localhost/rubinius",
- description: "",
- homepage: "localhost/rubinius",
- private: true
- }
- }
-
- described_class.create({
- project: project,
- action: described_class::PUSHED,
- data: data,
- author_id: user.id
- }.merge!(attrs))
+ def create_push_event(project, user)
+ event = create(:push_event, project: project, author: user)
+
+ create(:push_event_payload,
+ event: event,
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 0,
+ ref: 'master')
+
+ event
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index f1d1f37c78a..fa3e80ba062 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -149,7 +149,7 @@ describe ProjectMember do
describe 'notifications' do
describe '#after_accept_request' do
it 'calls NotificationService.new_project_member' do
- member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+ member = create(:project_member, user: create(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:new_project_member)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4a80f41cdc9..b07a287f6eb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -485,7 +485,7 @@ describe Project do
describe 'last_activity' do
it 'alias last_activity to last_event' do
- last_event = create(:event, project: project)
+ last_event = create(:event, :closed, project: project)
expect(project.last_activity).to eq(last_event)
end
@@ -493,7 +493,7 @@ describe Project do
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- new_event = create(:event, project: project, created_at: Time.now)
+ new_event = create(:event, :closed, project: project, created_at: Time.now)
project.reload
expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
diff --git a/spec/models/push_event_payload_spec.rb b/spec/models/push_event_payload_spec.rb
new file mode 100644
index 00000000000..a049ad35584
--- /dev/null
+++ b/spec/models/push_event_payload_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe PushEventPayload do
+ describe 'saving payloads' do
+ it 'does not allow commit messages longer than 70 characters' do
+ event = create(:push_event)
+ payload = build(:push_event_payload, event: event)
+
+ expect(payload).to be_valid
+
+ payload.commit_title = 'a' * 100
+
+ expect(payload).not_to be_valid
+ end
+ end
+end
diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb
new file mode 100644
index 00000000000..532fb024261
--- /dev/null
+++ b/spec/models/push_event_spec.rb
@@ -0,0 +1,202 @@
+require 'spec_helper'
+
+describe PushEvent do
+ let(:payload) { PushEventPayload.new }
+
+ let(:event) do
+ event = described_class.new
+
+ allow(event).to receive(:push_event_payload).and_return(payload)
+
+ event
+ end
+
+ describe '.sti_name' do
+ it 'returns Event::PUSHED' do
+ expect(described_class.sti_name).to eq(Event::PUSHED)
+ end
+ end
+
+ describe '#push?' do
+ it 'returns true' do
+ expect(event).to be_push
+ end
+ end
+
+ describe '#push_with_commits?' do
+ it 'returns true when both the first and last commit are present' do
+ allow(event).to receive(:commit_from).and_return('123')
+ allow(event).to receive(:commit_to).and_return('456')
+
+ expect(event).to be_push_with_commits
+ end
+
+ it 'returns false when the first commit is missing' do
+ allow(event).to receive(:commit_to).and_return('456')
+
+ expect(event).not_to be_push_with_commits
+ end
+
+ it 'returns false when the last commit is missing' do
+ allow(event).to receive(:commit_from).and_return('123')
+
+ expect(event).not_to be_push_with_commits
+ end
+ end
+
+ describe '#tag?' do
+ it 'returns true when pushing to a tag' do
+ allow(payload).to receive(:tag?).and_return(true)
+
+ expect(event).to be_tag
+ end
+
+ it 'returns false when pushing to a branch' do
+ allow(payload).to receive(:tag?).and_return(false)
+
+ expect(event).not_to be_tag
+ end
+ end
+
+ describe '#branch?' do
+ it 'returns true when pushing to a branch' do
+ allow(payload).to receive(:branch?).and_return(true)
+
+ expect(event).to be_branch
+ end
+
+ it 'returns false when pushing to a tag' do
+ allow(payload).to receive(:branch?).and_return(false)
+
+ expect(event).not_to be_branch
+ end
+ end
+
+ describe '#valid_push?' do
+ it 'returns true if a ref exists' do
+ allow(payload).to receive(:ref).and_return('master')
+
+ expect(event).to be_valid_push
+ end
+
+ it 'returns false when no ref is present' do
+ expect(event).not_to be_valid_push
+ end
+ end
+
+ describe '#new_ref?' do
+ it 'returns true when pushing a new ref' do
+ allow(payload).to receive(:created?).and_return(true)
+
+ expect(event).to be_new_ref
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ allow(payload).to receive(:created?).and_return(false)
+
+ expect(event).not_to be_new_ref
+ end
+ end
+
+ describe '#rm_ref?' do
+ it 'returns true when removing an existing ref' do
+ allow(payload).to receive(:removed?).and_return(true)
+
+ expect(event).to be_rm_ref
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ allow(payload).to receive(:removed?).and_return(false)
+
+ expect(event).not_to be_rm_ref
+ end
+ end
+
+ describe '#commit_from' do
+ it 'returns the first commit SHA' do
+ allow(payload).to receive(:commit_from).and_return('123')
+
+ expect(event.commit_from).to eq('123')
+ end
+ end
+
+ describe '#commit_to' do
+ it 'returns the last commit SHA' do
+ allow(payload).to receive(:commit_to).and_return('123')
+
+ expect(event.commit_to).to eq('123')
+ end
+ end
+
+ describe '#ref_name' do
+ it 'returns the name of the ref' do
+ allow(payload).to receive(:ref).and_return('master')
+
+ expect(event.ref_name).to eq('master')
+ end
+ end
+
+ describe '#ref_type' do
+ it 'returns the type of the ref' do
+ allow(payload).to receive(:ref_type).and_return('branch')
+
+ expect(event.ref_type).to eq('branch')
+ end
+ end
+
+ describe '#branch_name' do
+ it 'returns the name of the branch' do
+ allow(payload).to receive(:ref).and_return('master')
+
+ expect(event.branch_name).to eq('master')
+ end
+ end
+
+ describe '#tag_name' do
+ it 'returns the name of the tag' do
+ allow(payload).to receive(:ref).and_return('1.2')
+
+ expect(event.tag_name).to eq('1.2')
+ end
+ end
+
+ describe '#commit_title' do
+ it 'returns the commit message' do
+ allow(payload).to receive(:commit_title).and_return('foo')
+
+ expect(event.commit_title).to eq('foo')
+ end
+ end
+
+ describe '#commit_id' do
+ it 'returns the SHA of the last commit if present' do
+ allow(event).to receive(:commit_to).and_return('123')
+
+ expect(event.commit_id).to eq('123')
+ end
+
+ it 'returns the SHA of the first commit if the last commit is not present' do
+ allow(event).to receive(:commit_to).and_return(nil)
+ allow(event).to receive(:commit_from).and_return('123')
+
+ expect(event.commit_id).to eq('123')
+ end
+ end
+
+ describe '#commits_count' do
+ it 'returns the number of commits' do
+ allow(payload).to receive(:commit_count).and_return(1)
+
+ expect(event.commits_count).to eq(1)
+ end
+ end
+
+ describe '#validate_push_action' do
+ it 'adds an error when the action is not PUSHED' do
+ event.action = Event::CREATED
+ event.validate_push_action
+
+ expect(event.errors.count).to eq(1)
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6c8248eeb40..97bb91a6ac8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1291,7 +1291,7 @@ describe User do
let!(:project2) { create(:project, forked_from_project: project3) }
let!(:project3) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
- let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
+ let!(:push_event) { create(:push_event, project: project1, author: subject) }
let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
@@ -1333,10 +1333,18 @@ describe User do
subject { create(:user) }
let!(:project1) { create(:project, :repository) }
let!(:project2) { create(:project, :repository, forked_from_project: project1) }
- let!(:push_data) do
- Gitlab::DataBuilder::Push.build_sample(project2, subject)
+
+ let!(:push_event) do
+ event = create(:push_event, project: project2, author: subject)
+
+ create(:push_event_payload,
+ event: event,
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 0,
+ ref: 'master')
+
+ event
end
- let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
before do
project1.team << [subject, :master]
@@ -1363,8 +1371,13 @@ describe User do
expect(subject.recent_push(project1)).to eq(nil)
expect(subject.recent_push(project2)).to eq(push_event)
- push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
- push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
+ push_event1 = create(:push_event, project: project1, author: subject)
+
+ create(:push_event_payload,
+ event: push_event1,
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 0,
+ ref: 'master')
expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index f1a26b6ce6c..a23d28994ce 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -59,6 +59,34 @@ describe API::Events do
expect(json_response.size).to eq(1)
end
+ context 'when the list of events includes push events' do
+ let(:event) do
+ create(:push_event, author: user, project: private_project)
+ end
+
+ let!(:payload) { create(:push_event_payload, event: event) }
+ let(:payload_hash) { json_response[0]['push_data'] }
+
+ before do
+ get api("/users/#{user.id}/events?action=pushed", user)
+ end
+
+ it 'responds with HTTP 200 OK' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'includes the push payload as a Hash' do
+ expect(payload_hash).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the push payload details' do
+ expect(payload_hash['commit_count']).to eq(payload.commit_count)
+ expect(payload_hash['action']).to eq(payload.action)
+ expect(payload_hash['ref_type']).to eq(payload.ref_type)
+ expect(payload_hash['commit_to']).to eq(payload.commit_to)
+ end
+ end
+
context 'when there are multiple events from different projects' do
let(:second_note) { create(:note_on_issue, project: create(:project)) }
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index bc0a4ab20a3..227b8d1b0c1 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -252,6 +252,31 @@ describe API::V3::Users do
end
context "as a user than can see the event's project" do
+ context 'when the list of events includes push events' do
+ let(:event) { create(:push_event, author: user, project: project) }
+ let!(:payload) { create(:push_event_payload, event: event) }
+ let(:payload_hash) { json_response[0]['push_data'] }
+
+ before do
+ get api("/users/#{user.id}/events?action=pushed", user)
+ end
+
+ it 'responds with HTTP 200 OK' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'includes the push payload as a Hash' do
+ expect(payload_hash).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the push payload details' do
+ expect(payload_hash['commit_count']).to eq(payload.commit_count)
+ expect(payload_hash['action']).to eq(payload.action)
+ expect(payload_hash['ref_type']).to eq(payload.ref_type)
+ expect(payload_hash['commit_to']).to eq(payload.commit_to)
+ end
+ end
+
context 'joined event' do
it 'returns the "joined" event' do
get v3_api("/users/#{user.id}/events", user)
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 42adb044190..02d7ddeb86b 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -117,12 +117,52 @@ describe EventCreateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:push_data) do
+ {
+ commits: [
+ {
+ id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ message: 'This is a commit'
+ }
+ ],
+ before: '0000000000000000000000000000000000000000',
+ after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ total_commits_count: 1,
+ ref: 'refs/heads/my-branch'
+ }
+ end
+
it 'creates a new event' do
- expect { service.push(project, user, {}) }.to change { Event.count }
+ expect { service.push(project, user, push_data) }.to change { Event.count }
+ end
+
+ it 'creates the push event payload' do
+ expect(PushEventPayloadService).to receive(:new)
+ .with(an_instance_of(PushEvent), push_data)
+ .and_call_original
+
+ service.push(project, user, push_data)
end
it 'updates user last activity' do
- expect { service.push(project, user, {}) }.to change { user_activity(user) }
+ expect { service.push(project, user, push_data) }
+ .to change { user_activity(user) }
+ end
+
+ it 'does not create any event data when an error is raised' do
+ payload_service = double(:service)
+
+ allow(payload_service).to receive(:execute)
+ .and_raise(RuntimeError)
+
+ allow(PushEventPayloadService).to receive(:new)
+ .and_return(payload_service)
+
+ expect { service.push(project, user, push_data) }
+ .to raise_error(RuntimeError)
+
+ expect(Event.count).to eq(0)
+ expect(PushEventPayload.count).to eq(0)
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index a6449a3c9f5..8485605b398 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -141,10 +141,13 @@ describe GitPushService, services: true do
let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
let(:event) { Event.find_by_action(Event::PUSHED) }
- it { expect(event).not_to be_nil }
+ it { expect(event).to be_an_instance_of(PushEvent) }
it { expect(event.project).to eq(project) }
it { expect(event.action).to eq(Event::PUSHED) }
- it { expect(event.data).to eq(push_data) }
+ it { expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) }
+ it { expect(event.push_event_payload.commit_from).to eq(oldrev) }
+ it { expect(event.push_event_payload.commit_to).to eq(newrev) }
+ it { expect(event.push_event_payload.ref).to eq('master') }
context "Updates merge requests" do
it "when pushing a new branch for the first time" do
diff --git a/spec/services/push_event_payload_service_spec.rb b/spec/services/push_event_payload_service_spec.rb
new file mode 100644
index 00000000000..81956200bff
--- /dev/null
+++ b/spec/services/push_event_payload_service_spec.rb
@@ -0,0 +1,218 @@
+require 'spec_helper'
+
+describe PushEventPayloadService do
+ let(:event) { create(:push_event) }
+
+ describe '#execute' do
+ let(:push_data) do
+ {
+ commits: [
+ {
+ id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ message: 'This is a commit'
+ }
+ ],
+ before: '0000000000000000000000000000000000000000',
+ after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ total_commits_count: 1,
+ ref: 'refs/heads/my-branch'
+ }
+ end
+
+ it 'creates a new PushEventPayload row' do
+ payload = described_class.new(event, push_data).execute
+
+ expect(payload.commit_count).to eq(1)
+ expect(payload.action).to eq('created')
+ expect(payload.ref_type).to eq('branch')
+ expect(payload.commit_from).to be_nil
+ expect(payload.commit_to).to eq(push_data[:after])
+ expect(payload.ref).to eq('my-branch')
+ expect(payload.commit_title).to eq('This is a commit')
+ expect(payload.event_id).to eq(event.id)
+ end
+
+ it 'sets the push_event_payload association of the used event' do
+ payload = described_class.new(event, push_data).execute
+
+ expect(event.push_event_payload).to eq(payload)
+ end
+ end
+
+ describe '#commit_title' do
+ it 'returns nil if no commits were pushed' do
+ service = described_class.new(event, commits: [])
+
+ expect(service.commit_title).to be_nil
+ end
+
+ it 'returns a String limited to 70 characters' do
+ service = described_class.new(event, commits: [{ message: 'a' * 100 }])
+
+ expect(service.commit_title).to eq(('a' * 67) + '...')
+ end
+
+ it 'does not truncate the commit message if it is shorter than 70 characters' do
+ service = described_class.new(event, commits: [{ message: 'Hello' }])
+
+ expect(service.commit_title).to eq('Hello')
+ end
+
+ it 'includes the first line of a commit message if the message spans multiple lines' do
+ service = described_class
+ .new(event, commits: [{ message: "Hello\n\nworld" }])
+
+ expect(service.commit_title).to eq('Hello')
+ end
+ end
+
+ describe '#commit_from_id' do
+ it 'returns nil when creating a new ref' do
+ service = described_class.new(event, before: Gitlab::Git::BLANK_SHA)
+
+ expect(service.commit_from_id).to be_nil
+ end
+
+ it 'returns the ID of the first commit when pushing to an existing ref' do
+ service = described_class.new(event, before: '123')
+
+ expect(service.commit_from_id).to eq('123')
+ end
+ end
+
+ describe '#commit_to_id' do
+ it 'returns nil when removing an existing ref' do
+ service = described_class.new(event, after: Gitlab::Git::BLANK_SHA)
+
+ expect(service.commit_to_id).to be_nil
+ end
+ end
+
+ describe '#commit_count' do
+ it 'returns the number of commits' do
+ service = described_class.new(event, total_commits_count: 1)
+
+ expect(service.commit_count).to eq(1)
+ end
+
+ it 'raises when the push data does not contain the commits count' do
+ service = described_class.new(event, {})
+
+ expect { service.commit_count }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#ref' do
+ it 'returns the name of the ref' do
+ service = described_class.new(event, ref: 'refs/heads/foo')
+
+ expect(service.ref).to eq('refs/heads/foo')
+ end
+
+ it 'raises when the push data does not contain the ref name' do
+ service = described_class.new(event, {})
+
+ expect { service.ref }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#revision_before' do
+ it 'returns the revision from before the push' do
+ service = described_class.new(event, before: 'foo')
+
+ expect(service.revision_before).to eq('foo')
+ end
+
+ it 'raises when the push data does not contain the before revision' do
+ service = described_class.new(event, {})
+
+ expect { service.revision_before }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#revision_after' do
+ it 'returns the revision from after the push' do
+ service = described_class.new(event, after: 'foo')
+
+ expect(service.revision_after).to eq('foo')
+ end
+
+ it 'raises when the push data does not contain the after revision' do
+ service = described_class.new(event, {})
+
+ expect { service.revision_after }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#trimmed_ref' do
+ it 'returns the ref name without its prefix' do
+ service = described_class.new(event, ref: 'refs/heads/foo')
+
+ expect(service.trimmed_ref).to eq('foo')
+ end
+ end
+
+ describe '#create?' do
+ it 'returns true when creating a new ref' do
+ service = described_class.new(event, before: Gitlab::Git::BLANK_SHA)
+
+ expect(service.create?).to eq(true)
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ service = described_class.new(event, before: 'foo')
+
+ expect(service.create?).to eq(false)
+ end
+ end
+
+ describe '#remove?' do
+ it 'returns true when removing an existing ref' do
+ service = described_class.new(event, after: Gitlab::Git::BLANK_SHA)
+
+ expect(service.remove?).to eq(true)
+ end
+
+ it 'returns false pushing to an existing ref' do
+ service = described_class.new(event, after: 'foo')
+
+ expect(service.remove?).to eq(false)
+ end
+ end
+
+ describe '#action' do
+ it 'returns :created when creating a ref' do
+ service = described_class.new(event, before: Gitlab::Git::BLANK_SHA)
+
+ expect(service.action).to eq(:created)
+ end
+
+ it 'returns :removed when removing an existing ref' do
+ service = described_class.new(event,
+ before: '123',
+ after: Gitlab::Git::BLANK_SHA)
+
+ expect(service.action).to eq(:removed)
+ end
+
+ it 'returns :pushed when pushing to an existing ref' do
+ service = described_class.new(event, before: '123', after: '456')
+
+ expect(service.action).to eq(:pushed)
+ end
+ end
+
+ describe '#ref_type' do
+ it 'returns :tag for a tag' do
+ service = described_class.new(event, ref: 'refs/tags/1.2')
+
+ expect(service.ref_type).to eq(:tag)
+ end
+
+ it 'returns :branch for a branch' do
+ service = described_class.new(event, ref: 'refs/heads/master')
+
+ expect(service.ref_type).to eq(:branch)
+ end
+ end
+end
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
index 35e1518a35e..ea974355050 100644
--- a/spec/workers/prune_old_events_worker_spec.rb
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -2,9 +2,11 @@ require 'spec_helper'
describe PruneOldEventsWorker do
describe '#perform' do
- let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) }
- let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) }
- let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) }
+ let(:user) { create(:user) }
+
+ let!(:expired_event) { create(:event, :closed, author: user, created_at: 13.months.ago) }
+ let!(:not_expired_event) { create(:event, :closed, author: user, created_at: 1.day.ago) }
+ let!(:exactly_12_months_event) { create(:event, :closed, author: user, created_at: 12.months.ago) }
it 'prunes events older than 12 months' do
expect { subject.perform }.to change { Event.count }.by(-1)