From c0a4f527db7d3e10f843468522d574cdb5427e86 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Fri, 24 Feb 2017 17:53:44 +0200 Subject: [PATCH] Use Gitaly for CommitController#show --- Gemfile | 2 +- Gemfile.lock | 4 +- app/models/commit.rb | 9 ++- .../feature-use-gitaly-for-commit-show.yml | 4 ++ lib/gitlab/git/diff.rb | 21 ++++++- lib/gitlab/git/diff_collection.rb | 4 +- lib/gitlab/gitaly_client.rb | 14 +++++ lib/gitlab/gitaly_client/commit.rb | 25 ++++++++ spec/lib/gitlab/git/diff_spec.rb | 37 ++++++++++++ spec/lib/gitlab/gitaly_client/commit_spec.rb | 58 +++++++++++++++++++ spec/models/commit_spec.rb | 28 +++++++++ 11 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/feature-use-gitaly-for-commit-show.yml create mode 100644 lib/gitlab/gitaly_client/commit.rb create mode 100644 spec/lib/gitlab/gitaly_client/commit_spec.rb diff --git a/Gemfile b/Gemfile index 2f813324a35..6af27ce0f3e 100644 --- a/Gemfile +++ b/Gemfile @@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # Gitaly GRPC client -gem 'gitaly', '~> 0.2.1' +gem 'gitaly', '~> 0.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index e38f45b8e98..4b8919b2237 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -250,7 +250,7 @@ GEM json get_process_mem (0.2.0) gherkin-ruby (0.3.2) - gitaly (0.2.1) + gitaly (0.3.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -896,7 +896,7 @@ DEPENDENCIES fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.0) - gitaly (~> 0.2.1) + gitaly (~> 0.3.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) diff --git a/app/models/commit.rb b/app/models/commit.rb index 0a18986ef26..d5ecbe76c82 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -317,7 +317,14 @@ class Commit end def raw_diffs(*args) - raw.diffs(*args) + use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs) + deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only] + + if use_gitaly && !deltas_only + Gitlab::GitalyClient::Commit.diff_from_parent(self, *args) + else + raw.diffs(*args) + end end def diffs(diff_options = nil) diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml new file mode 100644 index 00000000000..4b668d994a1 --- /dev/null +++ b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml @@ -0,0 +1,4 @@ +--- +title: Use Gitaly for CommitController#show +merge_request: 9629 +author: diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index 2a017c93f57..019be151353 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -176,9 +176,13 @@ module Gitlab def initialize(raw_diff, collapse: false) case raw_diff when Hash - init_from_hash(raw_diff, collapse: collapse) + init_from_hash(raw_diff) + prune_diff_if_eligible(collapse) when Rugged::Patch, Rugged::Diff::Delta init_from_rugged(raw_diff, collapse: collapse) + when Gitaly::CommitDiffResponse + init_from_gitaly(raw_diff) + prune_diff_if_eligible(collapse) when nil raise "Nil as raw diff passed" else @@ -266,13 +270,26 @@ module Gitlab @diff = encode!(strip_diff_headers(patch.to_s)) end - def init_from_hash(hash, collapse: false) + def init_from_hash(hash) raw_diff = hash.symbolize_keys serialize_keys.each do |key| send(:"#{key}=", raw_diff[key.to_sym]) end + end + def init_from_gitaly(diff_msg) + @diff = diff_msg.raw_chunks.join + @new_path = encode!(diff_msg.to_path.dup) + @old_path = encode!(diff_msg.from_path.dup) + @a_mode = diff_msg.old_mode.to_s(8) + @b_mode = diff_msg.new_mode.to_s(8) + @new_file = diff_msg.from_id == BLANK_SHA + @renamed_file = diff_msg.from_path != diff_msg.to_path + @deleted_file = diff_msg.to_id == BLANK_SHA + end + + def prune_diff_if_eligible(collapse = false) prune_large_diff! if too_large? prune_collapsed_diff! if collapse && collapsible? end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 65e06f5065d..4e45ec7c174 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -30,7 +30,9 @@ module Gitlab elsif @deltas_only each_delta(&block) else - each_patch(&block) + Gitlab::GitalyClient.migrate(:commit_raw_diffs) do + each_patch(&block) + end end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index b981a629fb0..5534d4af439 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -25,5 +25,19 @@ module Gitlab def self.enabled? gitaly_address.present? end + + def self.feature_enabled?(feature) + enabled? && ENV["GITALY_#{feature.upcase}"] == '1' + end + + def self.migrate(feature) + is_enabled = feature_enabled?(feature) + metric_name = feature.to_s + metric_name += "_gitaly" if is_enabled + + Gitlab::Metrics.measure(metric_name) do + yield is_enabled + end + end end end diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb new file mode 100644 index 00000000000..525b8d680e9 --- /dev/null +++ b/lib/gitlab/gitaly_client/commit.rb @@ -0,0 +1,25 @@ +module Gitlab + module GitalyClient + class Commit + # The ID of empty tree. + # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012 + EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze + + class << self + def diff_from_parent(commit, options = {}) + stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: GitalyClient.channel) + repo = Gitaly::Repository.new(path: commit.project.repository.path_to_repo) + parent = commit.parents[0] + parent_id = parent ? parent.id : EMPTY_TREE_ID + request = Gitaly::CommitDiffRequest.new( + repository: repo, + left_commit_id: parent_id, + right_commit_id: commit.id + ) + + Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options) + end + end + end + end +end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 4c55532d165..992126ef153 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -109,6 +109,43 @@ EOT end end end + + context 'using a Gitaly::CommitDiffResponse' do + let(:diff) do + described_class.new( + Gitaly::CommitDiffResponse.new( + to_path: ".gitmodules", + from_path: ".gitmodules", + old_mode: 0100644, + new_mode: 0100644, + from_id: '357406f3075a57708d0163752905cc1576fceacc', + to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0', + raw_chunks: raw_chunks, + ) + ) + end + + context 'with a small diff' do + let(:raw_chunks) { [@raw_diff_hash[:diff]] } + + it 'initializes the diff' do + expect(diff.to_hash).to eq(@raw_diff_hash) + end + + it 'does not prune the diff' do + expect(diff).not_to be_too_large + end + end + + context 'using a diff that is too large' do + let(:raw_chunks) { ['a' * 204800] } + + it 'prunes the diff' do + expect(diff.diff).to be_empty + expect(diff).to be_too_large + end + end + end end describe 'straight diffs' do diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb new file mode 100644 index 00000000000..4684b1d1ac0 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::Commit do + describe '.diff_from_parent' do + let(:diff_stub) { double('Gitaly::Diff::Stub') } + let(:project) { create(:project, :repository) } + let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) } + let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') } + + before do + allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub) + allow(diff_stub).to receive(:commit_diff).and_return([]) + end + + context 'when a commit has a parent' do + it 'sends an RPC request with the parent ID as left commit' do + request = Gitaly::CommitDiffRequest.new( + repository: repository_message, + left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + right_commit_id: commit.id, + ) + + expect(diff_stub).to receive(:commit_diff).with(request) + + described_class.diff_from_parent(commit) + end + end + + context 'when a commit does not have a parent' do + it 'sends an RPC request with empty tree ref as left commit' do + initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') + request = Gitaly::CommitDiffRequest.new( + repository: repository_message, + left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904', + right_commit_id: initial_commit.id, + ) + + expect(diff_stub).to receive(:commit_diff).with(request) + + described_class.diff_from_parent(initial_commit) + end + end + + it 'returns a Gitlab::Git::DiffCollection' do + ret = described_class.diff_from_parent(commit) + + expect(ret).to be_kind_of(Gitlab::Git::DiffCollection) + end + + it 'passes options to Gitlab::Git::DiffCollection' do + options = { max_files: 31, max_lines: 13 } + + expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options) + + described_class.diff_from_parent(commit, options) + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 32f9366a14c..1d3591db446 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -369,4 +369,32 @@ eos expect(described_class.valid_hash?('a' * 41)).to be false end end + + describe '#raw_diffs' do + context 'Gitaly commit_raw_diffs feature enabled' do + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true) + end + + context 'when a truthy deltas_only is not passed to args' do + it 'fetches diffs from Gitaly server' do + expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent). + with(commit) + + commit.raw_diffs + end + end + + context 'when a truthy deltas_only is passed to args' do + it 'fetches diffs using Rugged' do + opts = { deltas_only: true } + + expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent) + expect(commit.raw).to receive(:diffs).with(opts) + + commit.raw_diffs(opts) + end + end + end + end end