diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index dab38858bf9..58ec8e75d7a 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -5,6 +5,7 @@ class SearchController < ApplicationController @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? @scope = params[:scope] + @show_snippets = params[:snippets].eql? 'true' @search_results = if @project return access_denied! unless can?(current_user, :download_code, @project) @@ -14,7 +15,7 @@ class SearchController < ApplicationController end Search::ProjectService.new(@project, current_user, params).execute - elsif params[:snippets].eql? 'true' + elsif @show_snippets unless %w(snippet_blobs snippet_titles).include?(@scope) @scope = 'snippet_blobs' end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index db2d7214077..c2c9301cc17 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -178,7 +178,7 @@ module ApplicationHelper def search_placeholder if @project && @project.persisted? "Search in this project" - elsif @snippet || @snippets || (params && params[:snippets] == 'true') + elsif @snippet || @snippets || @show_snippets 'Search snippets' elsif @group && @group.persisted? "Search in this group" diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index 2f71541a472..049aff0bc9b 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -1,36 +1,35 @@ -- unless params[:snippets] - .dropdown.inline - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %i.icon-tags - %span.light Group: - - if @group.present? - %strong= @group.name - - else +.dropdown.inline + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-tags + %span.light Group: + - if @group.present? + %strong= @group.name + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to search_filter_path(group_id: nil) do Any - %b.caret - %ul.dropdown-menu + - current_user.authorized_groups.sort_by(&:name).each do |group| %li - = link_to search_filter_path(group_id: nil) do - Any - - current_user.authorized_groups.sort_by(&:name).each do |group| - %li - = link_to search_filter_path(group_id: group.id, project_id: nil) do - = group.name + = link_to search_filter_path(group_id: group.id, project_id: nil) do + = group.name - .dropdown.inline.prepend-left-10.project-filter - %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} - %i.icon-tags - %span.light Project: - - if @project.present? - %strong= @project.name_with_namespace - - else +.dropdown.inline.prepend-left-10.project-filter + %a.dropdown-toggle.btn.btn-small{href: '#', "data-toggle" => "dropdown"} + %i.icon-tags + %span.light Project: + - if @project.present? + %strong= @project.name_with_namespace + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to search_filter_path(project_id: nil) do Any - %b.caret - %ul.dropdown-menu + - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| %li - = link_to search_filter_path(project_id: nil) do - Any - - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| - %li - = link_to search_filter_path(project_id: project.id, group_id: nil) do - = project.name_with_namespace + = link_to search_filter_path(project_id: project.id, group_id: nil) do + = project.name_with_namespace diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 83fd5ca10e5..58bcff9dbe3 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,6 +1,6 @@ %h4 #{@search_results.total_count} results found - - unless params[:snippets].eql? 'true' + - unless @show_snippets - if @project for #{link_to @project.name_with_namespace, @project} - elsif @group @@ -12,7 +12,7 @@ .col-sm-3 - if @project = render "project_filter" - - elsif params[:snippets].eql? 'true' + - elsif @show_snippets = render 'snippet_filter' - else = render "global_filter" diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 9deec490953..bae57917a4c 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -9,8 +9,9 @@ = submit_tag 'Search', class: "btn btn-create" .form-group .col-sm-2 - .col-sm-10 - = render 'filter', f: f + - unless params[:snippets].eql? 'true' + .col-sm-10 + = render 'filter', f: f = hidden_field_tag :project_id, params[:project_id] = hidden_field_tag :group_id, params[:group_id] = hidden_field_tag :snippets, params[:snippets] diff --git a/features/snippet_search.feature b/features/snippet_search.feature new file mode 100644 index 00000000000..834bd3b2376 --- /dev/null +++ b/features/snippet_search.feature @@ -0,0 +1,20 @@ +@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 diff --git a/features/steps/shared/search.rb b/features/steps/shared/search.rb new file mode 100644 index 00000000000..6c3d601763d --- /dev/null +++ b/features/steps/shared/search.rb @@ -0,0 +1,11 @@ +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 diff --git a/features/steps/shared/snippet.rb b/features/steps/shared/snippet.rb index 543e43196a5..5f89a3ccf65 100644 --- a/features/steps/shared/snippet.rb +++ b/features/steps/shared/snippet.rb @@ -18,4 +18,12 @@ module SharedSnippet private: true, author: current_user) end + And 'I have a public many lined snippet' do + create(:personal_snippet, + title: "Many lined snippet", + content: "line one\nline two\nline three\nline four\nline five\nline six\nline seven\nline eight\nline nine\nline ten\nline eleven\nline twelve\nline thirteen\nline fourteen", + file_name: "many_lined_snippet.rb", + private: true, + author: current_user) + end end diff --git a/features/steps/snippet_search.rb b/features/steps/snippet_search.rb new file mode 100644 index 00000000000..eb7d56c5f3f --- /dev/null +++ b/features/steps/snippet_search.rb @@ -0,0 +1,56 @@ +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 diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index 04217aab49f..938219efdb2 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -48,53 +48,84 @@ module Gitlab 'snippet_blobs' end - def bounded_line_numbers(line, min, max, surrounding_lines) + # 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 - def chunk_snippet(snippet) - surrounding_lines = 3 + # 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 = snippet.content.split("\n") lined_content.each_with_index do |line, line_number| used_lines.concat bounded_line_numbers( line_number, 0, - lined_content.size, - surrounding_lines + lined_content.size ) if line.include?(query) end - used_lines = used_lines.uniq.sort + 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