Commit 9e4a4098 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge pull request #7646 from bushong1/snippet-search3

Adding in snippet search functionality
parents 002ce69e 4561a09c
...@@ -5,6 +5,7 @@ class SearchController < ApplicationController ...@@ -5,6 +5,7 @@ class SearchController < ApplicationController
@project = Project.find_by(id: params[:project_id]) if params[:project_id].present? @project = Project.find_by(id: params[:project_id]) if params[:project_id].present?
@group = Group.find_by(id: params[:group_id]) if params[:group_id].present? @group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
@scope = params[:scope] @scope = params[:scope]
@show_snippets = params[:snippets].eql? 'true'
@search_results = if @project @search_results = if @project
return access_denied! unless can?(current_user, :download_code, @project) return access_denied! unless can?(current_user, :download_code, @project)
...@@ -14,6 +15,12 @@ class SearchController < ApplicationController ...@@ -14,6 +15,12 @@ class SearchController < ApplicationController
end end
Search::ProjectService.new(@project, current_user, params).execute Search::ProjectService.new(@project, current_user, params).execute
elsif @show_snippets
unless %w(snippet_blobs snippet_titles).include?(@scope)
@scope = 'snippet_blobs'
end
Search::SnippetService.new(current_user, params).execute
else else
unless %w(projects issues merge_requests).include?(@scope) unless %w(projects issues merge_requests).include?(@scope)
@scope = 'projects' @scope = 'projects'
......
...@@ -178,6 +178,8 @@ module ApplicationHelper ...@@ -178,6 +178,8 @@ module ApplicationHelper
def search_placeholder def search_placeholder
if @project && @project.persisted? if @project && @project.persisted?
"Search in this project" "Search in this project"
elsif @snippet || @snippets || @show_snippets
'Search snippets'
elsif @group && @group.persisted? elsif @group && @group.persisted?
"Search in this group" "Search in this group"
else else
......
...@@ -65,4 +65,18 @@ class Snippet < ActiveRecord::Base ...@@ -65,4 +65,18 @@ class Snippet < ActiveRecord::Base
def expired? def expired?
expires_at && expires_at < Time.current expires_at && expires_at < Time.current
end end
class << self
def search(query)
where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%")
end
def search_code(query)
where('(content LIKE :query)', query: "%#{query}%")
end
def accessible_to(user)
where('private = ? OR author_id = ?', false, user)
end
end
end end
module Search
class SnippetService
attr_accessor :current_user, :params
def initialize(user, params)
@current_user, @params = user, params.dup
end
def execute
snippet_ids = Snippet.accessible_to(current_user).pluck(:id)
Gitlab::SnippetSearchResults.new(snippet_ids, params[:search])
end
end
end
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
- if @project && @project.persisted? - if @project && @project.persisted?
= hidden_field_tag :project_id, @project.id = hidden_field_tag :project_id, @project.id
= hidden_field_tag :search_code, true = hidden_field_tag :search_code, true
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref = hidden_field_tag :repository_ref, @ref
= submit_tag 'Go' if ENV['RAILS_ENV'] == 'test' = submit_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
......
%h4 %h4
#{@search_results.total_count} results found #{@search_results.total_count} results found
- unless @show_snippets
- if @project - if @project
for #{link_to @project.name_with_namespace, @project} for #{link_to @project.name_with_namespace, @project}
- elsif @group - elsif @group
...@@ -11,6 +12,8 @@ ...@@ -11,6 +12,8 @@
.col-sm-3 .col-sm-3
- if @project - if @project
= render "project_filter" = render "project_filter"
- elsif @show_snippets
= render 'snippet_filter'
- else - else
= render "global_filter" = render "global_filter"
.col-sm-9 .col-sm-9
......
%ul.nav.nav-pills.nav-stacked.search-filter
%li{class: ("active" if @scope == 'snippet_blobs')}
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
%i.icon-code
Snippet Contents
.pull-right
= @search_results.snippet_blobs_count
%li{class: ("active" if @scope == 'snippet_titles')}
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
%i.icon-book
Titles and Filenames
.pull-right
= @search_results.snippet_titles_count
.search-result-row
%span
= snippet_blob[:snippet_object].title
by
= link_to user_snippets_path(snippet_blob[:snippet_object].author) do
= image_tag avatar_icon(snippet_blob[:snippet_object].author_email), class: "avatar avatar-inline s16", alt: ''
= snippet_blob[:snippet_object].author_name
%span.light #{time_ago_with_tooltip(snippet_blob[:snippet_object].created_at)}
%h4.snippet-title
- snippet_path = reliable_snippet_path(snippet_blob[:snippet_object])
= link_to snippet_path do
.file-holder
.file-title
%i.icon-file
%strong= snippet_blob[:snippet_object].file_name
%span.options
.btn-group.tree-btn-group.pull-right
- if snippet_blob[:snippet_object].author == current_user
= link_to "Edit", edit_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", title: 'Edit Snippet'
= link_to "Delete", snippet_path(snippet_blob[:snippet_object]), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-tiny", title: 'Delete Snippet'
= link_to "Raw", raw_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", target: "_blank"
- if gitlab_markdown?(snippet_blob[:snippet_object].file_name)
.file-content.wiki
- snippet_blob[:snippet_chunks].each do |snippet|
- unless snippet[:data].empty?
= preserve do
= markdown(snippet[:data])
- else
.file-content.code
.nothing-here-block Empty file
- elsif markup?(snippet_blob[:snippet_object].file_name)
.file-content.wiki
- snippet_blob[:snippet_chunks].each do |snippet|
- unless snippet[:data].empty?
= render_markup(snippet_blob[:snippet_object].file_name, snippet[:data])
- else
.file-content.code
.nothing-here-block Empty file
- else
.file-content.code
%div.highlighted-data{class: user_color_scheme_class}
.line-numbers
- snippet_blob[:snippet_chunks].each do |snippet|
- unless snippet[:data].empty?
- snippet[:data].lines.to_a.size.times do |index|
- offset = defined?(snippet[:start_line]) ? snippet[:start_line] : 1
- i = index + offset
= link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}" do
%i.icon-link
= i
- unless snippet == snippet_blob[:snippet_chunks].last
%a
= "."
.highlight.term
%pre
%code
- snippet_blob[:snippet_chunks].each do |snippet|
- unless snippet[:data].empty?
= snippet[:data]
- unless snippet == snippet_blob[:snippet_chunks].last
%a
= "..."
- else
.file-content.code
.nothing-here-block Empty file
.search-result-row
%h4.snippet-title.term
= link_to reliable_snippet_path(snippet_title) do
= truncate(snippet_title.title, length: 60)
- if snippet_title.private?
%span.label.label-gray
%i.icon-lock
private
%span.cgray.monospace.tiny.pull-right.term
= snippet_title.file_name
%small.pull-right.cgray
- if snippet_title.project_id?
= link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project)
.snippet-info
= "##{snippet_title.id}"
%span
by
= link_to user_snippets_path(snippet_title.author) do
= image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name
%span.light #{time_ago_with_tooltip(snippet_title.created_at)}
...@@ -9,10 +9,12 @@ ...@@ -9,10 +9,12 @@
= submit_tag 'Search', class: "btn btn-create" = submit_tag 'Search', class: "btn btn-create"
.form-group .form-group
.col-sm-2 .col-sm-2
- unless params[:snippets].eql? 'true'
.col-sm-10 .col-sm-10
= render 'filter', f: f = render 'filter', f: f
= hidden_field_tag :project_id, params[:project_id] = hidden_field_tag :project_id, params[:project_id]
= hidden_field_tag :group_id, params[:group_id] = hidden_field_tag :group_id, params[:group_id]
= hidden_field_tag :snippets, params[:snippets]
= hidden_field_tag :scope, params[:scope] = hidden_field_tag :scope, params[:scope]
.results.prepend-top-10 .results.prepend-top-10
......
@dashboard
Feature: Snippet Search
Background:
Given I sign in as a user
And I have public "Personal snippet one" snippet
And I have private "Personal snippet private" snippet
And I have a public many lined snippet
Scenario: I should see my public and private snippets
When I search for "snippet" in snippet titles
Then I should see "Personal snippet one" in results
And I should see "Personal snippet private" in results
Scenario: I should see three surrounding lines on either side of a matching snippet line
When I search for "line seven" in snippet contents
Then I should see "line four" in results
And I should see "line seven" in results
And I should see "line ten" in results
And I should not see "line three" in results
And I should not see "line eleven" in results
module SharedSearch
include Spinach::DSL
def search_snippet_contents(query)
visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_blobs"
end
def search_snippet_titles(query)
visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_titles"
end
end
...@@ -18,4 +18,27 @@ module SharedSnippet ...@@ -18,4 +18,27 @@ module SharedSnippet
private: true, private: true,
author: current_user) author: current_user)
end end
And 'I have a public many lined snippet' do
create(:personal_snippet,
title: 'Many lined snippet',
content: <<-END.gsub(/^\s+\|/, ''),
|line one
|line two
|line three
|line four
|line five
|line six
|line seven
|line eight
|line nine
|line ten
|line eleven
|line twelve
|line thirteen
|line fourteen
END
file_name: 'many_lined_snippet.rb',
private: true,
author: current_user)
end
end end
class Spinach::Features::SnippetSearch < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedSnippet
include SharedUser
include SharedSearch
step 'I search for "snippet" in snippet titles' do
search_snippet_titles 'snippet'
end
step 'I search for "snippet private" in snippet titles' do
search_snippet_titles 'snippet private'
end
step 'I search for "line seven" in snippet contents' do
search_snippet_contents 'line seven'
end
step 'I should see "line seven" in results' do
page.should have_content 'line seven'
end
step 'I should see "line four" in results' do
page.should have_content 'line four'
end
step 'I should see "line ten" in results' do
page.should have_content 'line ten'
end
step 'I should not see "line eleven" in results' do
page.should_not have_content 'line eleven'
end
step 'I should not see "line three" in results' do
page.should_not have_content 'line three'
end
Then 'I should see "Personal snippet one" in results' do
page.should have_content 'Personal snippet one'
end
And 'I should see "Personal snippet private" in results' do
page.should have_content 'Personal snippet private'
end
Then 'I should not see "Personal snippet one" in results' do
page.should_not have_content 'Personal snippet one'
end
And 'I should not see "Personal snippet private" in results' do
page.should_not have_content 'Personal snippet private'
end
end
module Gitlab
class SnippetSearchResults < SearchResults
attr_reader :limit_snippet_ids
def initialize(limit_snippet_ids, query)
@limit_snippet_ids = limit_snippet_ids
@query = query
end
def objects(scope, page = nil)
case scope
when 'snippet_titles'
Kaminari.paginate_array(snippet_titles).page(page).per(per_page)
when 'snippet_blobs'
Kaminari.paginate_array(snippet_blobs).page(page).per(per_page)
else
super
end
end
def total_count
@total_count ||= snippet_titles_count + snippet_blobs_count
end
def snippet_titles_count
@snippet_titles_count ||= snippet_titles.count
end
def snippet_blobs_count
@snippet_blobs_count ||= snippet_blobs.count
end
private
def snippet_titles
Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC')
end
def snippet_blobs
search = Snippet.where(id: limit_snippet_ids).search_code(query)
search = search.order('updated_at DESC').to_a
snippets = []
search.each { |e| snippets << chunk_snippet(e) }
snippets
end
def default_scope
'snippet_blobs'
end
# Get an array of line numbers surrounding a matching
# line, bounded by min/max.
#
# @returns Array of line numbers
def bounded_line_numbers(line, min, max)
lower = line - surrounding_lines > min ? line - surrounding_lines : min
upper = line + surrounding_lines < max ? line + surrounding_lines : max
(lower..upper).to_a
end
# Returns a sorted set of lines to be included in a snippet preview.
# This ensures matching adjacent lines do not display duplicated
# surrounding code.
#
# @returns Array, unique and sorted.
def matching_lines(lined_content)
used_lines = []
lined_content.each_with_index do |line, line_number|
used_lines.concat bounded_line_numbers(
line_number,
0,
lined_content.size
) if line.include?(query)
end
used_lines.uniq.sort
end
# 'Chunkify' entire snippet. Splits the snippet data into matching lines +
# surrounding_lines() worth of unmatching lines.
#
# @returns a hash with {snippet_object, snippet_chunks:{data,start_line}}
def chunk_snippet(snippet)
lined_content = snippet.content.split("\n")
used_lines = matching_lines(lined_content)
snippet_chunk = []
snippet_chunks = []
snippet_start_line = 0
last_line = -1
# Go through each used line, and add consecutive lines as a single chunk
# to the snippet chunk array.
used_lines.each do |line_number|
if last_line < 0
# Start a new chunk.
snippet_start_line = line_number
snippet_chunk << lined_content[line_number]
elsif last_line == line_number - 1
# Consecutive line, continue chunk.
snippet_chunk << lined_content[line_number]
else
# Non-consecutive line, add chunk to chunk array.
snippet_chunks << {
data: snippet_chunk.join("\n"),
start_line: snippet_start_line + 1
}
# Start a new chunk.
snippet_chunk = [lined_content[line_number]]
snippet_start_line = line_number
end
last_line = line_number
end
# Add final chunk to chunk array
snippet_chunks << {
data: snippet_chunk.join("\n"),
start_line: snippet_start_line + 1
}
# Return snippet with chunk array
{ snippet_object: snippet, snippet_chunks: snippet_chunks }
end
# Defines how many unmatching lines should be
# included around the matching lines in a snippet
def surrounding_lines
3
end
end
end
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