Commit 75d96967 authored by Vasilii Iakliushin's avatar Vasilii Iakliushin

Merge branch '225174-paginated-git-blame' into 'master'

Plumbing to allow blames to be limited or paginated

See merge request gitlab-org/gitlab!84388
parents ef3ad5dc 6d9b812e
...@@ -482,7 +482,7 @@ gem 'ssh_data', '~> 1.2' ...@@ -482,7 +482,7 @@ gem 'ssh_data', '~> 1.2'
gem 'spamcheck', '~> 0.1.0' gem 'spamcheck', '~> 0.1.0'
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 14.9.0-rc5' gem 'gitaly', '~> 14.10.0-rc1'
# KAS GRPC protocol definitions # KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.0.2' gem 'kas-grpc', '~> 0.0.2'
......
...@@ -455,7 +455,7 @@ GEM ...@@ -455,7 +455,7 @@ GEM
rails (>= 3.2.0) rails (>= 3.2.0)
git (1.7.0) git (1.7.0)
rchardet (~> 1.8) rchardet (~> 1.8)
gitaly (14.9.0.pre.rc5) gitaly (14.10.0.pre.rc1)
grpc (~> 1.0) grpc (~> 1.0)
github-markup (1.7.0) github-markup (1.7.0)
gitlab (4.16.1) gitlab (4.16.1)
...@@ -1494,7 +1494,7 @@ DEPENDENCIES ...@@ -1494,7 +1494,7 @@ DEPENDENCIES
gettext (~> 3.3) gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly (~> 14.9.0.pre.rc5) gitaly (~> 14.10.0.pre.rc1)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5) gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 3.0) gitlab-dangerfiles (~> 3.0)
......
...@@ -28,6 +28,10 @@ module Gitlab ...@@ -28,6 +28,10 @@ module Gitlab
precalculate_data_by_commit! precalculate_data_by_commit!
end end
def first_line
blame.first_line
end
def groups def groups
@groups ||= blame.groups @groups ||= blame.groups
end end
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
.table-responsive.file-content.blame.code{ class: user_color_scheme } .table-responsive.file-content.blame.code{ class: user_color_scheme }
%table %table
- current_line = 1 - current_line = @blame.first_line
- @blame.groups.each do |blame_group| - @blame.groups.each do |blame_group|
- commit_data = @blame.commit_data(blame_group[:commit]) - commit_data = @blame.commit_data(blame_group[:commit])
- line_count = blame_group[:lines].count - line_count = blame_group[:lines].count
......
...@@ -2,11 +2,16 @@ ...@@ -2,11 +2,16 @@
module Gitlab module Gitlab
class Blame class Blame
attr_accessor :blob, :commit attr_accessor :blob, :commit, :range
def initialize(blob, commit) def initialize(blob, commit, range: nil)
@blob = blob @blob = blob
@commit = commit @commit = commit
@range = range
end
def first_line
range&.first || 1
end end
def groups(highlight: true) def groups(highlight: true)
...@@ -14,7 +19,7 @@ module Gitlab ...@@ -14,7 +19,7 @@ module Gitlab
groups = [] groups = []
current_group = nil current_group = nil
i = 0 i = first_line - 1
blame.each do |commit, line, previous_path| blame.each do |commit, line, previous_path|
commit = Commit.new(commit, project) commit = Commit.new(commit, project)
commit.lazy_author # preload author commit.lazy_author # preload author
...@@ -37,7 +42,7 @@ module Gitlab ...@@ -37,7 +42,7 @@ module Gitlab
private private
def blame def blame
@blame ||= Gitlab::Git::Blame.new(repository, @commit.id, @blob.path) @blame ||= Gitlab::Git::Blame.new(repository, @commit.id, @blob.path, range: range)
end end
def highlighted_lines def highlighted_lines
......
...@@ -5,12 +5,13 @@ module Gitlab ...@@ -5,12 +5,13 @@ module Gitlab
class Blame class Blame
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
attr_reader :lines, :blames attr_reader :lines, :blames, :range
def initialize(repository, sha, path) def initialize(repository, sha, path, range: nil)
@repo = repository @repo = repository
@sha = sha @sha = sha
@path = path @path = path
@range = range
@lines = [] @lines = []
@blames = load_blame @blames = load_blame
end end
...@@ -23,13 +24,20 @@ module Gitlab ...@@ -23,13 +24,20 @@ module Gitlab
private private
def range_spec
"#{range.first},#{range.last}" if range
end
def load_blame def load_blame
output = encode_utf8(@repo.gitaly_commit_client.raw_blame(@sha, @path)) output = encode_utf8(
@repo.gitaly_commit_client.raw_blame(@sha, @path, range: range_spec)
)
process_raw_blame(output) process_raw_blame(output)
end end
def process_raw_blame(output) def process_raw_blame(output)
start_line = nil
lines = [] lines = []
final = [] final = []
info = {} info = {}
...@@ -47,6 +55,10 @@ module Gitlab ...@@ -47,6 +55,10 @@ module Gitlab
commit_id = m[1] commit_id = m[1]
commits[commit_id] = nil unless commits.key?(commit_id) commits[commit_id] = nil unless commits.key?(commit_id)
info[m[3].to_i] = [commit_id, m[2].to_i] info[m[3].to_i] = [commit_id, m[2].to_i]
# Assumption: the first line returned by git blame is lowest-numbered
# This is true unless we start passing it `--incremental`.
start_line = m[3].to_i if start_line.nil?
elsif line.start_with?("previous ") elsif line.start_with?("previous ")
# previous 1485b69e7b839a21436e81be6d3aa70def5ed341 initial-commit # previous 1485b69e7b839a21436e81be6d3aa70def5ed341 initial-commit
# previous 9521e52704ee6100e7d2a76896a4ef0eb53ff1b8 "\303\2511\\\303\251\\303\\251\n" # previous 9521e52704ee6100e7d2a76896a4ef0eb53ff1b8 "\303\2511\\\303\251\\303\\251\n"
...@@ -61,7 +73,13 @@ module Gitlab ...@@ -61,7 +73,13 @@ module Gitlab
# get it together # get it together
info.sort.each do |lineno, (commit_id, old_lineno)| info.sort.each do |lineno, (commit_id, old_lineno)|
final << BlameLine.new(lineno, old_lineno, commits[commit_id], lines[lineno - 1], previous_paths[commit_id]) final << BlameLine.new(
lineno,
old_lineno,
commits[commit_id],
lines[lineno - start_line],
previous_paths[commit_id]
)
end end
@lines = final @lines = final
......
...@@ -315,11 +315,12 @@ module Gitlab ...@@ -315,11 +315,12 @@ module Gitlab
response.languages.map { |l| { value: l.share.round(2), label: l.name, color: l.color, highlight: l.color } } response.languages.map { |l| { value: l.share.round(2), label: l.name, color: l.color, highlight: l.color } }
end end
def raw_blame(revision, path) def raw_blame(revision, path, range:)
request = Gitaly::RawBlameRequest.new( request = Gitaly::RawBlameRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
revision: encode_binary(revision), revision: encode_binary(revision),
path: encode_binary(path) path: encode_binary(path),
range: (encode_binary(range) if range)
) )
response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout) response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request, timeout: GitalyClient.medium_timeout)
......
...@@ -3,13 +3,31 @@ ...@@ -3,13 +3,31 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Blame do RSpec.describe Gitlab::Blame do
let(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:path) { 'files/ruby/popen.rb' } let(:path) { 'files/ruby/popen.rb' }
let(:commit) { project.commit('master') } let(:commit) { project.commit('master') }
let(:blob) { project.repository.blob_at(commit.id, path) } let(:blob) { project.repository.blob_at(commit.id, path) }
let(:range) { nil }
subject(:blame) { described_class.new(blob, commit, range: range) }
describe '#first_line' do
subject { blame.first_line }
it { is_expected.to eq(1) }
context 'with a range' do
let(:range) { 2..3 }
it { is_expected.to eq(range.first) }
end
end
describe "#groups" do describe "#groups" do
let(:subject) { described_class.new(blob, commit).groups(highlight: false) } let(:highlighted) { false }
subject(:groups) { blame.groups(highlight: highlighted) }
it 'groups lines properly' do it 'groups lines properly' do
expect(subject.count).to eq(18) expect(subject.count).to eq(18)
...@@ -23,6 +41,50 @@ RSpec.describe Gitlab::Blame do ...@@ -23,6 +41,50 @@ RSpec.describe Gitlab::Blame do
expect(subject[-1][:lines]).to eq([" end", "end"]) expect(subject[-1][:lines]).to eq([" end", "end"])
end end
context 'with a range 1..5' do
let(:range) { 1..5 }
it 'returns the correct lines' do
expect(groups.count).to eq(2)
expect(groups[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""])
expect(groups[1][:lines]).to eq(['module Popen', ' extend self'])
end
context 'with highlighted lines' do
let(:highlighted) { true }
it 'returns the correct lines' do
expect(groups.count).to eq(2)
expect(groups[0][:lines][0]).to match(/LC1.*fileutils/)
expect(groups[0][:lines][1]).to match(/LC2.*open3/)
expect(groups[0][:lines][2]).to eq("<span id=\"LC3\" class=\"line\" lang=\"ruby\"></span>\n")
expect(groups[1][:lines][0]).to match(/LC4.*Popen/)
expect(groups[1][:lines][1]).to match(/LC5.*extend/)
end
end
end
context 'with a range 2..4' do
let(:range) { 2..4 }
it 'returns the correct lines' do
expect(groups.count).to eq(2)
expect(groups[0][:lines]).to eq(["require 'open3'", ""])
expect(groups[1][:lines]).to eq(['module Popen'])
end
context 'with highlighted lines' do
let(:highlighted) { true }
it 'returns the correct lines' do
expect(groups.count).to eq(2)
expect(groups[0][:lines][0]).to match(/LC2.*open3/)
expect(groups[0][:lines][1]).to eq("<span id=\"LC3\" class=\"line\" lang=\"ruby\"></span>\n")
expect(groups[1][:lines][0]).to match(/LC4.*Popen/)
end
end
end
context 'renamed file' do context 'renamed file' do
let(:path) { 'files/plain_text/renamed' } let(:path) { 'files/plain_text/renamed' }
let(:commit) { project.commit('blame-on-renamed') } let(:commit) { project.commit('blame-on-renamed') }
......
...@@ -4,106 +4,81 @@ require "spec_helper" ...@@ -4,106 +4,81 @@ require "spec_helper"
RSpec.describe Gitlab::Git::Blame, :seed_helper do RSpec.describe Gitlab::Git::Blame, :seed_helper do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:blame) do
Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") let(:sha) { SeedRepo::Commit::ID }
let(:path) { 'CONTRIBUTING.md' }
let(:range) { nil }
subject(:blame) { Gitlab::Git::Blame.new(repository, sha, path, range: range) }
let(:result) do
[].tap do |data|
blame.each do |commit, line, previous_path|
data << { commit: commit, line: line, previous_path: previous_path }
end
end
end end
describe 'blaming a file' do describe 'blaming a file' do
context "each count" do it 'has the right number of lines' do
it do expect(result.size).to eq(95)
data = [] expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
blame.each do |commit, line| expect(result.first[:line]).to eq("# Contribute to GitLab")
data << { expect(result.first[:line]).to be_utf8
commit: commit, end
line: line
} context 'blaming a range' do
end let(:range) { 2..4 }
expect(data.size).to eq(95) it 'only returns the range' do
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) expect(result.size).to eq(range.size)
expect(data.first[:line]).to eq("# Contribute to GitLab") expect(result.map {|r| r[:line] }).to eq(['', 'This guide details how contribute to GitLab.', ''])
expect(data.first[:line]).to be_utf8
end end
end end
context "ISO-8859 encoding" do context "ISO-8859 encoding" do
let(:blame) do let(:sha) { SeedRepo::EncodingCommit::ID }
Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") let(:path) { 'encoding/iso8859.txt' }
end
it 'converts to UTF-8' do it 'converts to UTF-8' do
data = [] expect(result.size).to eq(1)
blame.each do |commit, line| expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
data << { expect(result.first[:line]).to eq("Ä ü")
commit: commit, expect(result.first[:line]).to be_utf8
line: line
}
end
expect(data.size).to eq(1)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq("Ä ü")
expect(data.first[:line]).to be_utf8
end end
end end
context "unknown encoding" do context "unknown encoding" do
let(:blame) do let(:sha) { SeedRepo::EncodingCommit::ID }
Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") let(:path) { 'encoding/iso8859.txt' }
end
it 'converts to UTF-8' do it 'converts to UTF-8' do
expect_next_instance_of(CharlockHolmes::EncodingDetector) do |detector| expect_next_instance_of(CharlockHolmes::EncodingDetector) do |detector|
expect(detector).to receive(:detect).and_return(nil) expect(detector).to receive(:detect).and_return(nil)
end end
data = [] expect(result.size).to eq(1)
blame.each do |commit, line| expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
data << { expect(result.first[:line]).to eq(" ")
commit: commit, expect(result.first[:line]).to be_utf8
line: line
}
end
expect(data.size).to eq(1)
expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
expect(data.first[:line]).to eq(" ")
expect(data.first[:line]).to be_utf8
end end
end end
context "renamed file" do context "renamed file" do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
let(:commit) { project.commit('blame-on-renamed') } let(:commit) { project.commit('blame-on-renamed') }
let(:sha) { commit.id }
let(:path) { 'files/plain_text/renamed' } let(:path) { 'files/plain_text/renamed' }
let(:blame) { described_class.new(project.repository, commit.id, path) } it 'includes the previous path' do
expect(result.size).to eq(5)
it do
data = []
blame.each do |commit, line, previous_path|
data << {
commit: commit,
line: line,
previous_path: previous_path
}
end
expect(data.size).to eq(5)
expect(data[0][:line]).to eq('Initial commit')
expect(data[0][:previous_path]).to be nil
expect(data[1][:line]).to eq('Initial commit')
expect(data[1][:previous_path]).to be nil
expect(data[2][:line]).to eq('Renamed as "filename"')
expect(data[2][:previous_path]).to eq('files/plain_text/initial-commit')
expect(data[3][:line]).to eq('Renamed as renamed')
expect(data[3][:previous_path]).to eq('files/plain_text/"filename"')
expect(data[4][:line]).to eq('Last edit, no rename') expect(result[0]).to include(line: 'Initial commit', previous_path: nil)
expect(data[4][:previous_path]).to eq('files/plain_text/renamed') expect(result[1]).to include(line: 'Initial commit', previous_path: nil)
expect(result[2]).to include(line: 'Renamed as "filename"', previous_path: 'files/plain_text/initial-commit')
expect(result[3]).to include(line: 'Renamed as renamed', previous_path: 'files/plain_text/"filename"')
expect(result[4]).to include(line: 'Last edit, no rename', previous_path: path)
end end
end end
end end
......
...@@ -563,4 +563,39 @@ RSpec.describe Gitlab::GitalyClient::CommitService do ...@@ -563,4 +563,39 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
expect(response).not_to have_key 'nonexistent' expect(response).not_to have_key 'nonexistent'
end end
end end
describe '#raw_blame' do
let(:project) { create(:project, :test_repo) }
let(:revision) { 'blame-on-renamed' }
let(:path) { 'files/plain_text/renamed' }
let(:blame_headers) do
[
'405a45736a75e439bb059e638afaa9a3c2eeda79 1 1 2',
'405a45736a75e439bb059e638afaa9a3c2eeda79 2 2',
'bed1d1610ebab382830ee888288bf939c43873bb 3 3 1',
'3685515c40444faf92774e72835e1f9c0e809672 4 4 1',
'32c33da59f8a1a9f90bdeda570337888b00b244d 5 5 1'
]
end
subject(:blame) { client.raw_blame(revision, path, range: range).split("\n") }
context 'without a range' do
let(:range) { nil }
it 'blames a whole file' do
is_expected.to include(*blame_headers)
end
end
context 'with a range' do
let(:range) { '3,4' }
it 'blames part of a file' do
is_expected.to include(blame_headers[2], blame_headers[3])
is_expected.not_to include(blame_headers[0], blame_headers[1], blame_headers[4])
end
end
end
end end
...@@ -27,6 +27,14 @@ RSpec.describe Gitlab::BlamePresenter do ...@@ -27,6 +27,14 @@ RSpec.describe Gitlab::BlamePresenter do
end end
end end
describe '#first_line' do
it 'delegates #first_line call to the blame' do
expect(blame).to receive(:first_line).at_least(:once).and_call_original
subject.first_line
end
end
describe '#commit_data' do describe '#commit_data' do
it 'has the data necessary to render the view' do it 'has the data necessary to render the view' do
commit = blame.groups.first[:commit] commit = blame.groups.first[:commit]
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment