diff options
author | Charles Bushong <bushong1@gmail.com> | 2014-08-29 15:22:45 -0400 |
---|---|---|
committer | Charles Bushong <bushong1@gmail.com> | 2014-08-29 15:32:07 -0400 |
commit | 4cca1b050a0e80e4ce6bb67f530549a2f28af630 (patch) | |
tree | 31d5c5897f7b5e041a921ba221042d1a9bb6a0e1 | |
parent | 8a7d10af2329342ca003300847ace4793c66f854 (diff) | |
download | gitlab-ce-4cca1b050a0e80e4ce6bb67f530549a2f28af630.tar.gz |
Adding in snippet search functionality
http://feedback.gitlab.com/forums/176466-general/suggestions/5529795-search-though-snippets
-rw-r--r-- | app/controllers/search_controller.rb | 6 | ||||
-rw-r--r-- | app/helpers/application_helper.rb | 2 | ||||
-rw-r--r-- | app/models/snippet.rb | 14 | ||||
-rw-r--r-- | app/services/search/snippet_service.rb | 14 | ||||
-rw-r--r-- | app/views/layouts/_search.html.haml | 2 | ||||
-rw-r--r-- | app/views/search/_filter.html.haml | 61 | ||||
-rw-r--r-- | app/views/search/_results.html.haml | 11 | ||||
-rw-r--r-- | app/views/search/_snippet_filter.html.haml | 13 | ||||
-rw-r--r-- | app/views/search/results/_snippet_blob.html.haml | 65 | ||||
-rw-r--r-- | app/views/search/results/_snippet_title.html.haml | 23 | ||||
-rw-r--r-- | app/views/search/show.html.haml | 1 | ||||
-rw-r--r-- | lib/gitlab/snippet_search_results.rb | 100 |
12 files changed, 278 insertions, 34 deletions
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index a58b24de643..dab38858bf9 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -14,6 +14,12 @@ class SearchController < ApplicationController end Search::ProjectService.new(@project, current_user, params).execute + elsif params[:snippets].eql? 'true' + unless %w(snippet_blobs snippet_titles).include?(@scope) + @scope = 'snippet_blobs' + end + + Search::SnippetService.new(current_user, params).execute else unless %w(projects issues merge_requests).include?(@scope) @scope = 'projects' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e6d50bea4d1..db2d7214077 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -178,6 +178,8 @@ module ApplicationHelper def search_placeholder if @project && @project.persisted? "Search in this project" + elsif @snippet || @snippets || (params && params[:snippets] == 'true') + 'Search snippets' elsif @group && @group.persisted? "Search in this group" else diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 2c38e7939bd..80c1af8f337 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -65,4 +65,18 @@ class Snippet < ActiveRecord::Base def expired? expires_at && expires_at < Time.current 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 diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb new file mode 100644 index 00000000000..8ca0877321d --- /dev/null +++ b/app/services/search/snippet_service.rb @@ -0,0 +1,14 @@ +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 diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index caf0e39234a..d2257f6a671 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -5,6 +5,8 @@ - if @project && @project.persisted? = hidden_field_tag :project_id, @project.id = hidden_field_tag :search_code, true + - if @snippet || @snippets + = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref = 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 } diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index 049aff0bc9b..2f71541a472 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -1,35 +1,36 @@ -.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 +- 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 Any - - current_user.authorized_groups.sort_by(&:name).each do |group| + %b.caret + %ul.dropdown-menu %li - = link_to search_filter_path(group_id: group.id, project_id: nil) do - = group.name + = 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 -.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 + .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 - - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| + %b.caret + %ul.dropdown-menu %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: 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 diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index f9c0a6d61ff..83fd5ca10e5 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,9 +1,10 @@ %h4 #{@search_results.total_count} results found - - if @project - for #{link_to @project.name_with_namespace, @project} - - elsif @group - for #{link_to @group.name, @group} + - unless params[:snippets].eql? 'true' + - if @project + for #{link_to @project.name_with_namespace, @project} + - elsif @group + for #{link_to @group.name, @group} %hr @@ -11,6 +12,8 @@ .col-sm-3 - if @project = render "project_filter" + - elsif params[:snippets].eql? 'true' + = render 'snippet_filter' - else = render "global_filter" .col-sm-9 diff --git a/app/views/search/_snippet_filter.html.haml b/app/views/search/_snippet_filter.html.haml new file mode 100644 index 00000000000..45155a77f1a --- /dev/null +++ b/app/views/search/_snippet_filter.html.haml @@ -0,0 +1,13 @@ +%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 + Code + .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 diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml new file mode 100644 index 00000000000..a3d909d44dc --- /dev/null +++ b/app/views/search/results/_snippet_blob.html.haml @@ -0,0 +1,65 @@ +.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 diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml new file mode 100644 index 00000000000..84abb9293b2 --- /dev/null +++ b/app/views/search/results/_snippet_title.html.haml @@ -0,0 +1,23 @@ +.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)} diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 8d1614bfbd4..9deec490953 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -13,6 +13,7 @@ = 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] = hidden_field_tag :scope, params[:scope] .results.prepend-top-10 diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb new file mode 100644 index 00000000000..4b406c30f47 --- /dev/null +++ b/lib/gitlab/snippet_search_results.rb @@ -0,0 +1,100 @@ +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 + matching_snippets = Snippet.where(id: limit_snippet_ids).search_code(query).order('updated_at DESC') + matching_snippets = matching_snippets.to_a + snippets = [] + matching_snippets.each { |e| snippets << chunk_snippet(e) } + snippets + end + + def default_scope + 'snippet_blobs' + end + + def bounded_line_numbers(line, min, max, surrounding_lines) + 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 + used_lines = [] + lined_content = snippet.content.split("\n") + lined_content.each_with_index { |line, line_number| + used_lines.concat bounded_line_numbers( + line_number, + 0, + lined_content.size, + surrounding_lines + ) if line.include?(query) + } + + used_lines = used_lines.uniq.sort + + snippet_chunk = [] + snippet_chunks = [] + snippet_start_line = 0 + last_line = -1 + used_lines.each { |line_number| + if last_line < 0 + snippet_start_line = line_number + snippet_chunk << lined_content[line_number] + elsif last_line == line_number - 1 + snippet_chunk << lined_content[line_number] + else + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + snippet_chunk = [lined_content[line_number]] + snippet_start_line = line_number + end + last_line = line_number + } + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + + { snippet_object: snippet, snippet_chunks: snippet_chunks } + end + end +end |