summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Bushong <bushong1@gmail.com>2014-08-29 15:22:45 -0400
committerCharles Bushong <bushong1@gmail.com>2014-08-29 15:32:07 -0400
commit4cca1b050a0e80e4ce6bb67f530549a2f28af630 (patch)
tree31d5c5897f7b5e041a921ba221042d1a9bb6a0e1
parent8a7d10af2329342ca003300847ace4793c66f854 (diff)
downloadgitlab-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.rb6
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/models/snippet.rb14
-rw-r--r--app/services/search/snippet_service.rb14
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/search/_filter.html.haml61
-rw-r--r--app/views/search/_results.html.haml11
-rw-r--r--app/views/search/_snippet_filter.html.haml13
-rw-r--r--app/views/search/results/_snippet_blob.html.haml65
-rw-r--r--app/views/search/results/_snippet_title.html.haml23
-rw-r--r--app/views/search/show.html.haml1
-rw-r--r--lib/gitlab/snippet_search_results.rb100
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