Commit 891465ba authored by Sean McGivern's avatar Sean McGivern

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
parents 28d96459 a5632e80
...@@ -8,6 +8,10 @@ ...@@ -8,6 +8,10 @@
border-bottom: none; border-bottom: none;
} }
} }
.blob-result {
margin: 5px 0;
}
} }
.search { .search {
......
...@@ -31,34 +31,7 @@ module SearchHelper ...@@ -31,34 +31,7 @@ module SearchHelper
end end
def parse_search_result(result) def parse_search_result(result)
ref = nil Gitlab::ProjectSearchResults.parse_search_result(result)
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 end
private private
......
...@@ -879,7 +879,7 @@ class Project < ActiveRecord::Base ...@@ -879,7 +879,7 @@ class Project < ActiveRecord::Base
end end
def empty_repo? def empty_repo?
!repository.exists? || !repository.has_visible_content? repository.empty_repo?
end end
def repo def repo
......
...@@ -127,7 +127,7 @@ class ProjectWiki ...@@ -127,7 +127,7 @@ class ProjectWiki
end end
def search_files(query) def search_files(query)
repository.search_files(query, default_branch) repository.search_files_by_content(query, default_branch)
end end
def repository def repository
......
...@@ -1063,16 +1063,25 @@ class Repository ...@@ -1063,16 +1063,25 @@ class Repository
merge_base(ancestor_id, descendant_id) == ancestor_id merge_base(ancestor_id, descendant_id) == ancestor_id
end end
def search_files(query, ref) def empty_repo?
unless exists? && has_visible_content? && query.present? !exists? || !has_visible_content?
return []
end end
def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank?
offset = 2 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}) 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(/^--$/) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end 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) 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}) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo) Gitlab::Popen.popen(args, path_to_repo)
......
- blob = parse_search_result(blob) - file_name, blob = blob
.blob-result .blob-result
.file-holder .file-holder
.file-title .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 = link_to blob_link do
%i.fa.fa-file %i.fa.fa-file
%strong %strong
= blob.filename = file_name
- if blob
.file-content.code.term .file-content.code.term
= render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
---
title: Search for a filename in a project
merge_request:
author:
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
def initialize(current_user, project, query, repository_ref = nil) def initialize(current_user, project, query, repository_ref = nil)
@current_user = current_user @current_user = current_user
@project = project @project = project
@repository_ref = repository_ref.presence @repository_ref = repository_ref.presence || project.default_branch
@query = query @query = query
end end
...@@ -40,10 +40,57 @@ module Gitlab ...@@ -40,10 +40,57 @@ module Gitlab
@commits_count ||= commits.count @commits_count ||= commits.count
end 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 private
def blobs 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 end
def wiki_blobs def wiki_blobs
......
...@@ -6,38 +6,6 @@ describe SearchHelper do ...@@ -6,38 +6,6 @@ describe SearchHelper do
str str
end 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 describe 'search_autocomplete_source' do
context "with no current user" do context "with no current user" do
before do before do
......
...@@ -6,22 +6,65 @@ describe Gitlab::ProjectSearchResults, lib: true do ...@@ -6,22 +6,65 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:query) { 'hello world' } let(:query) { 'hello world' }
describe 'initialize with empty ref' do 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.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
it { expect(results.query).to eq('hello world') } it { expect(results.query).to eq('hello world') }
end end
describe 'initialize with ref' do describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' } 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.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) } it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') } it { expect(results.query).to eq('hello world') }
end 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 describe 'confidential issues' do
let(:query) { 'issue' } let(:query) { 'issue' }
let(:author) { create(:user) } let(:author) { create(:user) }
...@@ -66,7 +109,7 @@ describe Gitlab::ProjectSearchResults, lib: true do ...@@ -66,7 +109,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end end
it 'lists project confidential issues for assignee' do 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') issues = results.objects('issues')
expect(issues).to include issue expect(issues).to include issue
......
...@@ -393,33 +393,33 @@ describe Repository, models: true do ...@@ -393,33 +393,33 @@ describe Repository, models: true do
end end
end end
describe "search_files" do describe "search_files_by_content" do
let(:results) { repository.search_files('feature', 'master') } let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results } subject { results }
it { is_expected.to be_an Array } it { is_expected.to be_an Array }
it 'regex-escapes the query string' do 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:') expect(results.first).not_to start_with('fatal:')
end end
it 'properly handles an unmatched parenthesis' do 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:') expect(results.first).not_to start_with('fatal:')
end end
it 'properly handles when query is not present' do 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([]) expect(results).to match_array([])
end end
it 'properly handles query when repo is empty' do it 'properly handles query when repo is empty' do
repository = create(:empty_project).repository repository = create(:empty_project).repository
results = repository.search_files('test', 'master') results = repository.search_files_by_content('test', 'master')
expect(results).to match_array([]) expect(results).to match_array([])
end end
...@@ -432,6 +432,28 @@ describe Repository, models: true do ...@@ -432,6 +432,28 @@ describe Repository, models: true do
end end
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 describe '#create_ref' do
it 'redirects the call to fetch_ref' do it 'redirects the call to fetch_ref' do
ref, ref_path = '1', '2' ref, ref_path = '1', '2'
......
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