Merge branch '23117-search-for-a-filename-in-a-project' into 'master'
Search for a filename in a project Closes #23117 See merge request !7426
This commit is contained in:
commit
891465ba8c
|
@ -8,6 +8,10 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.blob-result {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
|
|
|
@ -31,34 +31,7 @@ module SearchHelper
|
|||
end
|
||||
|
||||
def parse_search_result(result)
|
||||
ref = nil
|
||||
filename = nil
|
||||
basename = nil
|
||||
startline = 0
|
||||
|
||||
result.each_line.each_with_index do |line, index|
|
||||
if line =~ /^.*:.*:\d+:/
|
||||
ref, filename, startline = line.split(':')
|
||||
startline = startline.to_i - index
|
||||
extname = Regexp.escape(File.extname(filename))
|
||||
basename = filename.sub(/#{extname}$/, '')
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
data = ""
|
||||
|
||||
result.each_line do |line|
|
||||
data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
|
||||
end
|
||||
|
||||
OpenStruct.new(
|
||||
filename: filename,
|
||||
basename: basename,
|
||||
ref: ref,
|
||||
startline: startline,
|
||||
data: data
|
||||
)
|
||||
Gitlab::ProjectSearchResults.parse_search_result(result)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -879,7 +879,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def empty_repo?
|
||||
!repository.exists? || !repository.has_visible_content?
|
||||
repository.empty_repo?
|
||||
end
|
||||
|
||||
def repo
|
||||
|
|
|
@ -127,7 +127,7 @@ class ProjectWiki
|
|||
end
|
||||
|
||||
def search_files(query)
|
||||
repository.search_files(query, default_branch)
|
||||
repository.search_files_by_content(query, default_branch)
|
||||
end
|
||||
|
||||
def repository
|
||||
|
|
|
@ -1063,16 +1063,25 @@ class Repository
|
|||
merge_base(ancestor_id, descendant_id) == ancestor_id
|
||||
end
|
||||
|
||||
def search_files(query, ref)
|
||||
unless exists? && has_visible_content? && query.present?
|
||||
return []
|
||||
end
|
||||
def empty_repo?
|
||||
!exists? || !has_visible_content?
|
||||
end
|
||||
|
||||
def search_files_by_content(query, ref)
|
||||
return [] if empty_repo? || query.blank?
|
||||
|
||||
offset = 2
|
||||
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
|
||||
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
|
||||
end
|
||||
|
||||
def search_files_by_name(query, ref)
|
||||
return [] if empty_repo? || query.blank?
|
||||
|
||||
args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
|
||||
Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
|
||||
end
|
||||
|
||||
def fetch_ref(source_path, source_ref, target_ref)
|
||||
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
|
||||
Gitlab::Popen.popen(args, path_to_repo)
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
- blob = parse_search_result(blob)
|
||||
- file_name, blob = blob
|
||||
.blob-result
|
||||
.file-holder
|
||||
.file-title
|
||||
- blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename))
|
||||
- ref = @search_results.repository_ref
|
||||
- blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
|
||||
= link_to blob_link do
|
||||
%i.fa.fa-file
|
||||
%strong
|
||||
= blob.filename
|
||||
.file-content.code.term
|
||||
= render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
|
||||
= file_name
|
||||
- if blob
|
||||
.file-content.code.term
|
||||
= render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Search for a filename in a project
|
||||
merge_request:
|
||||
author:
|
|
@ -5,7 +5,7 @@ module Gitlab
|
|||
def initialize(current_user, project, query, repository_ref = nil)
|
||||
@current_user = current_user
|
||||
@project = project
|
||||
@repository_ref = repository_ref.presence
|
||||
@repository_ref = repository_ref.presence || project.default_branch
|
||||
@query = query
|
||||
end
|
||||
|
||||
|
@ -40,10 +40,57 @@ module Gitlab
|
|||
@commits_count ||= commits.count
|
||||
end
|
||||
|
||||
def self.parse_search_result(result)
|
||||
ref = nil
|
||||
filename = nil
|
||||
basename = nil
|
||||
startline = 0
|
||||
|
||||
result.each_line.each_with_index do |line, index|
|
||||
if line =~ /^.*:.*:\d+:/
|
||||
ref, filename, startline = line.split(':')
|
||||
startline = startline.to_i - index
|
||||
extname = Regexp.escape(File.extname(filename))
|
||||
basename = filename.sub(/#{extname}$/, '')
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
data = ""
|
||||
|
||||
result.each_line do |line|
|
||||
data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
|
||||
end
|
||||
|
||||
OpenStruct.new(
|
||||
filename: filename,
|
||||
basename: basename,
|
||||
ref: ref,
|
||||
startline: startline,
|
||||
data: data
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blobs
|
||||
@blobs ||= project.repository.search_files(query, repository_ref)
|
||||
@blobs ||= begin
|
||||
blobs = project.repository.search_files_by_content(query, repository_ref).first(100)
|
||||
found_file_names = Set.new
|
||||
|
||||
results = blobs.map do |blob|
|
||||
blob = self.class.parse_search_result(blob)
|
||||
found_file_names << blob.filename
|
||||
|
||||
[blob.filename, blob]
|
||||
end
|
||||
|
||||
project.repository.search_files_by_name(query, repository_ref).first(100).each do |filename|
|
||||
results << [filename, nil] unless found_file_names.include?(filename)
|
||||
end
|
||||
|
||||
results.sort_by(&:first)
|
||||
end
|
||||
end
|
||||
|
||||
def wiki_blobs
|
||||
|
|
|
@ -6,38 +6,6 @@ describe SearchHelper do
|
|||
str
|
||||
end
|
||||
|
||||
describe 'parsing result' do
|
||||
let(:project) { create(:project) }
|
||||
let(:repository) { project.repository }
|
||||
let(:results) { repository.search_files('feature', 'master') }
|
||||
let(:search_result) { results.first }
|
||||
|
||||
subject { helper.parse_search_result(search_result) }
|
||||
|
||||
it "returns a valid OpenStruct object" do
|
||||
is_expected.to be_an OpenStruct
|
||||
expect(subject.filename).to eq('CHANGELOG')
|
||||
expect(subject.basename).to eq('CHANGELOG')
|
||||
expect(subject.ref).to eq('master')
|
||||
expect(subject.startline).to eq(188)
|
||||
expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
|
||||
end
|
||||
|
||||
context "when filename has extension" do
|
||||
let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
|
||||
|
||||
it { expect(subject.filename).to eq('CONTRIBUTE.md') }
|
||||
it { expect(subject.basename).to eq('CONTRIBUTE') }
|
||||
end
|
||||
|
||||
context "when file under directory" do
|
||||
let(:search_result) { "master:a/b/c.md:5:a b c\n" }
|
||||
|
||||
it { expect(subject.filename).to eq('a/b/c.md') }
|
||||
it { expect(subject.basename).to eq('a/b/c') }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'search_autocomplete_source' do
|
||||
context "with no current user" do
|
||||
before do
|
||||
|
|
|
@ -6,22 +6,65 @@ describe Gitlab::ProjectSearchResults, lib: true do
|
|||
let(:query) { 'hello world' }
|
||||
|
||||
describe 'initialize with empty ref' do
|
||||
let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') }
|
||||
let(:results) { described_class.new(user, project, query, '') }
|
||||
|
||||
it { expect(results.project).to eq(project) }
|
||||
it { expect(results.repository_ref).to be_nil }
|
||||
it { expect(results.query).to eq('hello world') }
|
||||
end
|
||||
|
||||
describe 'initialize with ref' do
|
||||
let(:ref) { 'refs/heads/test' }
|
||||
let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) }
|
||||
let(:results) { described_class.new(user, project, query, ref) }
|
||||
|
||||
it { expect(results.project).to eq(project) }
|
||||
it { expect(results.repository_ref).to eq(ref) }
|
||||
it { expect(results.query).to eq('hello world') }
|
||||
end
|
||||
|
||||
describe 'blob search' do
|
||||
let(:results) { described_class.new(user, project, 'files').objects('blobs') }
|
||||
|
||||
it 'finds by name' do
|
||||
expect(results).to include(["files/images/wm.svg", nil])
|
||||
end
|
||||
|
||||
it 'finds by content' do
|
||||
blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
|
||||
|
||||
expect(blob.filename).to eq("CHANGELOG")
|
||||
end
|
||||
|
||||
describe 'parsing results' do
|
||||
let(:results) { project.repository.search_files_by_content('feature', 'master') }
|
||||
let(:search_result) { results.first }
|
||||
|
||||
subject { described_class.parse_search_result(search_result) }
|
||||
|
||||
it "returns a valid OpenStruct object" do
|
||||
is_expected.to be_an OpenStruct
|
||||
expect(subject.filename).to eq('CHANGELOG')
|
||||
expect(subject.basename).to eq('CHANGELOG')
|
||||
expect(subject.ref).to eq('master')
|
||||
expect(subject.startline).to eq(188)
|
||||
expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
|
||||
end
|
||||
|
||||
context "when filename has extension" do
|
||||
let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
|
||||
|
||||
it { expect(subject.filename).to eq('CONTRIBUTE.md') }
|
||||
it { expect(subject.basename).to eq('CONTRIBUTE') }
|
||||
end
|
||||
|
||||
context "when file under directory" do
|
||||
let(:search_result) { "master:a/b/c.md:5:a b c\n" }
|
||||
|
||||
it { expect(subject.filename).to eq('a/b/c.md') }
|
||||
it { expect(subject.basename).to eq('a/b/c') }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'confidential issues' do
|
||||
let(:query) { 'issue' }
|
||||
let(:author) { create(:user) }
|
||||
|
@ -66,7 +109,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
|
|||
end
|
||||
|
||||
it 'lists project confidential issues for assignee' do
|
||||
results = described_class.new(assignee, project.id, query)
|
||||
results = described_class.new(assignee, project, query)
|
||||
issues = results.objects('issues')
|
||||
|
||||
expect(issues).to include issue
|
||||
|
|
|
@ -393,33 +393,33 @@ describe Repository, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe "search_files" do
|
||||
let(:results) { repository.search_files('feature', 'master') }
|
||||
describe "search_files_by_content" do
|
||||
let(:results) { repository.search_files_by_content('feature', 'master') }
|
||||
subject { results }
|
||||
|
||||
it { is_expected.to be_an Array }
|
||||
|
||||
it 'regex-escapes the query string' do
|
||||
results = repository.search_files("test\\", 'master')
|
||||
results = repository.search_files_by_content("test\\", 'master')
|
||||
|
||||
expect(results.first).not_to start_with('fatal:')
|
||||
end
|
||||
|
||||
it 'properly handles an unmatched parenthesis' do
|
||||
results = repository.search_files("test(", 'master')
|
||||
results = repository.search_files_by_content("test(", 'master')
|
||||
|
||||
expect(results.first).not_to start_with('fatal:')
|
||||
end
|
||||
|
||||
it 'properly handles when query is not present' do
|
||||
results = repository.search_files('', 'master')
|
||||
results = repository.search_files_by_content('', 'master')
|
||||
|
||||
expect(results).to match_array([])
|
||||
end
|
||||
|
||||
it 'properly handles query when repo is empty' do
|
||||
repository = create(:empty_project).repository
|
||||
results = repository.search_files('test', 'master')
|
||||
results = repository.search_files_by_content('test', 'master')
|
||||
|
||||
expect(results).to match_array([])
|
||||
end
|
||||
|
@ -432,6 +432,28 @@ describe Repository, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe "search_files_by_name" do
|
||||
let(:results) { repository.search_files_by_name('files', 'master') }
|
||||
|
||||
it 'returns result' do
|
||||
expect(results.first).to eq('files/html/500.html')
|
||||
end
|
||||
|
||||
it 'properly handles when query is not present' do
|
||||
results = repository.search_files_by_name('', 'master')
|
||||
|
||||
expect(results).to match_array([])
|
||||
end
|
||||
|
||||
it 'properly handles query when repo is empty' do
|
||||
repository = create(:empty_project).repository
|
||||
|
||||
results = repository.search_files_by_name('test', 'master')
|
||||
|
||||
expect(results).to match_array([])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_ref' do
|
||||
it 'redirects the call to fetch_ref' do
|
||||
ref, ref_path = '1', '2'
|
||||
|
|
Loading…
Reference in New Issue