diff --git a/app/models/concerns/cached_commit.rb b/app/models/concerns/cached_commit.rb index 8a53fec061261dee9bb058203f1a6488e49dd3f0..74120f49d01677f030a9816ce4066f3e14f56fc7 100644 --- a/app/models/concerns/cached_commit.rb +++ b/app/models/concerns/cached_commit.rb @@ -19,4 +19,8 @@ def parent_ids def referenced_by [] end + + def extended_trailers + {} + end end diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb index 281e11c7c136d2591e042bf336ad121f7c534587..921ad7e1f0afdd5cc8d1435c8f5ef70c91c4069f 100644 --- a/app/models/merge_request_context_commit.rb +++ b/app/models/merge_request_context_commit.rb @@ -26,6 +26,13 @@ def self.delete_bulk(merge_request, commits) # create MergeRequestContextCommit by given commit sha and it's diff file record def self.bulk_insert(rows, **args) + # Remove the new extended_trailers attribute as this shouldn't be + # inserted into the database. This will be removed once the old + # format of the trailers attribute is deprecated. + rows = rows.map do |row| + row.except(:extended_trailers).to_hash + end + ApplicationRecord.legacy_bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert end diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 790520c412356935cb7e15b02af461a8543760d8..d0d9f1733463aec33f0da274fdb540f088a49b8a 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -53,8 +53,13 @@ def self.create_bulk(merge_request_diff_id, commits) # These fields are only used to determine the author/committer IDs, we # don't store them in the DB. + # + # Trailers are stored in the DB here in order to allow changelog parsing. + # Rather than add an additional column for :extended_trailers, we're instead + # ignoring it for now until we deprecate the :trailers field and replace it with + # the new functionality. commit_hash = commit_hash - .except(:author_name, :author_email, :committer_name, :committer_email) + .except(:author_name, :author_email, :committer_name, :committer_email, :extended_trailers) commit_hash.merge( commit_author_id: author.id, diff --git a/lib/api/entities/commit.rb b/lib/api/entities/commit.rb index ab1f51289d7835f2c952e362c98b71e1db9dc94a..99ae4b66f674e63b3d36d07c04e2f0c0e29c240e 100644 --- a/lib/api/entities/commit.rb +++ b/lib/api/entities/commit.rb @@ -17,6 +17,10 @@ class Commit < Grape::Entity expose :committer_email, documentation: { type: 'string', example: 'jack@example.com' } expose :committed_date, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } expose :trailers, documentation: { type: 'object', example: '{ "Merged-By": "Jane Doe janedoe@gitlab.com" }' } + expose :extended_trailers, documentation: { + type: 'object', + example: '{ "Signed-off-by": ["John Doe <johndoe@gitlab.com>", "Jane Doe <janedoe@gitlab.com>"] }' + } expose :web_url, documentation: { diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 1086ea45a7a0f3f8102e0bebf60ee35b995499ca..d899ed3ba25f4ecd45ed118d8cff029cbcc4ce1d 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -28,7 +28,8 @@ class Commit SERIALIZE_KEYS = [ :id, :message, :parent_ids, :authored_date, :author_name, :author_email, - :committed_date, :committer_name, :committer_email, :trailers, :referenced_by + :committed_date, :committer_name, :committer_email, + :trailers, :extended_trailers, :referenced_by ].freeze attr_accessor(*SERIALIZE_KEYS) @@ -432,9 +433,17 @@ def init_from_gitaly(commit) @committer_email = commit.committer.email.dup @parent_ids = Array(commit.parent_ids) @trailers = commit.trailers.to_h { |t| [t.key, t.value] } + @extended_trailers = parse_commit_trailers(commit.trailers) @referenced_by = Array(commit.referenced_by) end + # Turn the commit trailers into a hash of key: [value, value] arrays + def parse_commit_trailers(trailers) + trailers.each_with_object({}) do |trailer, hash| + (hash[trailer.key] ||= []) << trailer.value + end + end + # Gitaly provides a UNIX timestamp in author.date.seconds, and a timezone # offset in author.timezone. If the latter isn't present, assume UTC. def init_date_from_gitaly(author) diff --git a/spec/factories/gitaly/commit.rb b/spec/factories/gitaly/commit.rb index 4e8220e449a58df604cca5524a6d65e6cc6c5148..ecf3e4e065e5e9bc0bf74c494a29b0ba78458150 100644 --- a/spec/factories/gitaly/commit.rb +++ b/spec/factories/gitaly/commit.rb @@ -16,5 +16,15 @@ body { subject + "\nMy body" } author { association(:gitaly_commit_author) } committer { association(:gitaly_commit_author) } + + trailers do + trailers = body.lines.keep_if { |l| l =~ /.*: / }.map do |l| + key, value = *l.split(":").map(&:strip) + + Gitaly::CommitTrailer.new(key: key, value: value) + end + + Google::Protobuf::RepeatedField.new(:message, Gitaly::CommitTrailer, trailers) + end end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index d8d62ac9670c8572fef54f21a63d1c01ab40b99f..6c8634281ae223c58ed641496c3accf9708de207 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -8,7 +8,6 @@ describe "Commit info from gitaly commit" do let(:subject) { (+"My commit").force_encoding('ASCII-8BIT') } - let(:body) { subject + (+"My body").force_encoding('ASCII-8BIT') } let(:body_size) { body.length } let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body, body_size: body_size, tree_id: tree_id) } let(:id) { gitaly_commit.id } @@ -17,6 +16,17 @@ let(:author) { gitaly_commit.author } let(:commit) { described_class.new(repository, gitaly_commit) } + let(:body) do + body = +<<~BODY + Bleep bloop. + + Cc: John Doe <johndoe@gitlab.com> + Cc: Jane Doe <janedoe@gitlab.com> + BODY + + [subject, "\n", body].join.force_encoding("ASCII-8BIT") + end + it { expect(commit.short_id).to eq(id[0..10]) } it { expect(commit.id).to eq(id) } it { expect(commit.sha).to eq(id) } @@ -29,6 +39,18 @@ it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) } it { expect(commit.tree_id).to eq(tree_id) } + it "parses the commit trailers" do + expect(commit.trailers).to eq( + { "Cc" => "Jane Doe <janedoe@gitlab.com>" } + ) + end + + it "parses the extended commit trailers" do + expect(commit.extended_trailers).to eq( + { "Cc" => ["John Doe <johndoe@gitlab.com>", "Jane Doe <janedoe@gitlab.com>"] } + ) + end + context 'non-UTC dates' do let(:seconds) { Time.now.to_i } @@ -773,6 +795,7 @@ def sample_commit_hash message: "tree css fixes", parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"], trailers: {}, + extended_trailers: {}, referenced_by: [] } end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 6a11291828822a448466476136c72ab1cb3a71a5..4ec5d195ff8b2ceea9a0a402b8c9bdf46b16a024 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -44,6 +44,30 @@ expect(response).to include_limited_pagination_headers end + + describe "commit trailers" do + it "doesn't include the commit trailers by default" do + get api(route, current_user), params: { page: 2 } + + commit_with_trailers = json_response.find { |c| c["trailers"].any? } + + expect(commit_with_trailers).to be_nil + expect(json_response.first["trailers"]).to eq({}) + end + + it "does include the commit trailers when specified in the params" do + # Test repo commits with trailers are further down the list, so use a + # higher page number. + get api(route, current_user), params: { page: 2, trailers: true } + + commit_with_trailers = json_response.find { |c| c["trailers"].any? } + + expect(commit_with_trailers["trailers"]).to be_a(Hash) + expect(commit_with_trailers["extended_trailers"]).to be_a(Hash) + expect(commit_with_trailers["trailers"].size).to be > 0 + expect(commit_with_trailers["extended_trailers"].size).to be > 0 + end + end end context 'when unauthenticated', 'and project is public' do @@ -426,6 +450,10 @@ expect(commit['trailers']).to eq( 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>' ) + + expect(commit['extended_trailers']).to eq( + 'Signed-off-by' => ['Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'] + ) end end end