From a5632e802b72db01c0fb0b8bec77c0fc28b41427 Mon Sep 17 00:00:00 2001
From: Valery Sizov <valery@gitlab.com>
Date: Thu, 10 Nov 2016 19:27:09 +0200
Subject: [PATCH] Search for a filename in a project

---
 app/assets/stylesheets/pages/search.scss      |  4 ++
 app/helpers/search_helper.rb                  | 29 +----------
 app/models/project.rb                         |  2 +-
 app/models/project_wiki.rb                    |  2 +-
 app/models/repository.rb                      | 17 +++++--
 app/views/search/results/_blob.html.haml      | 12 +++--
 ...117-search-for-a-filename-in-a-project.yml |  4 ++
 lib/gitlab/project_search_results.rb          | 51 ++++++++++++++++++-
 spec/helpers/search_helper_spec.rb            | 32 ------------
 .../lib/gitlab/project_search_results_spec.rb | 51 +++++++++++++++++--
 spec/models/repository_spec.rb                | 34 ++++++++++---
 11 files changed, 155 insertions(+), 83 deletions(-)
 create mode 100644 changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml

diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 1a116f6a919..49f65fe4901 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -8,6 +8,10 @@
       border-bottom: none;
     }
   }
+
+  .blob-result {
+    margin: 5px 0;
+  }
 }
 
 .search {
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index aba3a3f9c5d..cdb9663877c 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -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
diff --git a/app/models/project.rb b/app/models/project.rb
index bab2f0c53ca..327f81412f8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -879,7 +879,7 @@ class Project < ActiveRecord::Base
   end
 
   def empty_repo?
-    !repository.exists? || !repository.has_visible_content?
+    repository.empty_repo?
   end
 
   def repo
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 46f70da2452..9db96347322 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -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
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 4282197faa5..a035768a6c1 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -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)
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 6f0a0ea36ec..9e8adc82583 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -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
diff --git a/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml b/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml
new file mode 100644
index 00000000000..156f8d779ca
--- /dev/null
+++ b/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml
@@ -0,0 +1,4 @@
+---
+title: Search for a filename in a project
+merge_request: 
+author: 
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index b8326a64b22..66e6b29e798 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -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
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 64aa41020c9..4b2ca3514f8 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 29abb4d4d07..a0fdad87eee 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -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
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index fe26b4ac18c..3bd5741f2b7 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -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'
-- 
GitLab