summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js61
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js57
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_options.js12
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss17
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss9
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss94
-rw-r--r--app/assets/stylesheets/framework/selects.scss9
-rw-r--r--app/assets/stylesheets/pages/issues.scss8
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss18
-rw-r--r--app/helpers/issuables_helper.rb8
-rw-r--r--app/helpers/issues_helper.rb27
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/repository.rb106
-rw-r--r--app/models/user.rb9
-rw-r--r--app/services/commits/create_service.rb2
-rw-r--r--app/services/compare_service.rb22
-rw-r--r--app/services/git_operation_service.rb159
-rw-r--r--app/views/ci/lints/_create.html.haml4
-rw-r--r--app/views/feature_highlight/_issue_boards.svg98
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml14
-rw-r--r--app/views/shared/icons/_thumbs_up.svg1
26 files changed, 424 insertions, 329 deletions
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
new file mode 100644
index 00000000000..800ca05cd11
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -0,0 +1,61 @@
+import Cookies from 'js-cookie';
+import _ from 'underscore';
+import {
+ getCookieName,
+ getSelector,
+ hidePopover,
+ setupDismissButton,
+ mouseenter,
+ mouseleave,
+} from './feature_highlight_helper';
+
+export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => {
+ const $selector = $(getSelector(id));
+ const $parent = $selector.parent();
+ const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
+ const hideOnScroll = hidePopover.bind($selector);
+ const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
+
+ $selector
+ // Setup popover
+ .data('content', $popoverContent.prop('outerHTML'))
+ .popover({
+ html: true,
+ // Override the existing template to add custom CSS classes
+ template: `
+ <div class="popover feature-highlight-popover" role="tooltip">
+ <div class="arrow"></div>
+ <div class="popover-content"></div>
+ </div>
+ `,
+ })
+ .on('mouseenter', mouseenter)
+ .on('mouseleave', debouncedMouseleave)
+ .on('inserted.bs.popover', setupDismissButton)
+ .on('show.bs.popover', () => {
+ window.addEventListener('scroll', hideOnScroll);
+ })
+ .on('hide.bs.popover', () => {
+ window.removeEventListener('scroll', hideOnScroll);
+ })
+ // Display feature highlight
+ .removeAttr('disabled');
+};
+
+export const shouldHighlightFeature = (id) => {
+ const element = document.querySelector(getSelector(id));
+ const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true';
+
+ return element && !previouslyDismissed;
+};
+
+export const highlightFeatures = (highlightOrder) => {
+ const featureId = highlightOrder.find(shouldHighlightFeature);
+
+ if (featureId) {
+ setupFeatureHighlightPopover(featureId);
+ return true;
+ }
+
+ return false;
+};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
new file mode 100644
index 00000000000..9f741355cd7
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -0,0 +1,57 @@
+import Cookies from 'js-cookie';
+
+export const getCookieName = cookieId => `feature-highlighted-${cookieId}`;
+export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
+
+export const showPopover = function showPopover() {
+ if (this.hasClass('js-popover-show')) {
+ return false;
+ }
+ this.popover('show');
+ this.addClass('disable-animation js-popover-show');
+
+ return true;
+};
+
+export const hidePopover = function hidePopover() {
+ if (!this.hasClass('js-popover-show')) {
+ return false;
+ }
+ this.popover('hide');
+ this.removeClass('disable-animation js-popover-show');
+
+ return true;
+};
+
+export const dismiss = function dismiss(cookieId) {
+ Cookies.set(getCookieName(cookieId), true);
+ hidePopover.call(this);
+ this.hide();
+};
+
+export const mouseleave = function mouseleave() {
+ if (!$('.popover:hover').length > 0) {
+ const $featureHighlight = $(this);
+ hidePopover.call($featureHighlight);
+ }
+};
+
+export const mouseenter = function mouseenter() {
+ const $featureHighlight = $(this);
+
+ const showedPopover = showPopover.call($featureHighlight);
+ if (showedPopover) {
+ $('.popover')
+ .on('mouseleave', mouseleave.bind($featureHighlight));
+ }
+};
+
+export const setupDismissButton = function setupDismissButton() {
+ const popoverId = this.getAttribute('aria-describedby');
+ const cookieId = this.dataset.highlight;
+ const $popover = $(this);
+ const dismissWrapper = dismiss.bind($popover, cookieId);
+
+ $(`#${popoverId} .dismiss-feature-highlight`)
+ .on('click', dismissWrapper);
+};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
new file mode 100644
index 00000000000..fd48f2e87cc
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
@@ -0,0 +1,12 @@
+import { highlightFeatures } from './feature_highlight';
+import bp from '../breakpoints';
+
+const highlightOrder = ['issue-boards'];
+
+export default function domContentLoaded(order) {
+ if (bp.getBreakpointSize() === 'lg') {
+ highlightFeatures(order);
+ }
+}
+
+document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder));
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 928394f4fae..66f8cbd7139 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -102,6 +102,7 @@ import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
+import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index b2b3297e880..c0524bf6aa3 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -51,3 +51,4 @@
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive-tables";
+@import "framework/feature_highlight";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index b4a6b214e98..82350c36df0 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -46,6 +46,15 @@
}
}
+@mixin btn-svg {
+ svg {
+ height: 15px;
+ width: 15px;
+ position: relative;
+ top: 2px;
+ }
+}
+
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
background-color: $light;
border-color: $border-light;
@@ -123,6 +132,7 @@
.btn {
@include btn-default;
@include btn-white;
+ @include btn-svg;
color: $gl-text-color;
@@ -222,13 +232,6 @@
}
}
- svg {
- height: 15px;
- width: 15px;
- position: relative;
- top: 2px;
- }
-
svg,
.fa {
&:not(:last-child) {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 12fc3867c32..b0d6c2d9d22 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -737,6 +737,8 @@
@mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav {
+ margin-bottom: 24px;
+
li {
display: block;
padding: 0 1px;
@@ -768,7 +770,7 @@
// make sure the text color is not overriden
&.text-danger {
- // @extend .text-danger;
+ color: $brand-danger;
}
&.is-focused,
@@ -777,6 +779,11 @@
&:focus {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
+
+ // make sure the text color is not overriden
+ &.text-danger {
+ color: $brand-danger;
+ }
}
&.is-active {
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
new file mode 100644
index 00000000000..ebae473df50
--- /dev/null
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -0,0 +1,94 @@
+.feature-highlight {
+ position: relative;
+ margin-left: $gl-padding;
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+
+ &::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ width: 8px;
+ height: 8px;
+ background-color: $blue-500;
+ border-radius: 50%;
+ box-shadow: 0 0 0 rgba($blue-500, 0.4);
+ animation: pulse-highlight 2s infinite;
+ }
+
+ &:hover::before,
+ &.disable-animation::before {
+ animation: none;
+ }
+
+ &[disabled]::before {
+ display: none;
+ }
+}
+
+.is-showing-fly-out {
+ .feature-highlight {
+ display: none;
+ }
+}
+
+.feature-highlight-popover-content {
+ display: none;
+
+ hr {
+ margin: $gl-padding * 0.5 0;
+ }
+
+ .btn-link {
+ @include btn-svg;
+
+ svg path {
+ fill: currentColor;
+ }
+ }
+
+ .dismiss-feature-highlight {
+ padding: 0;
+ }
+
+ svg:first-child {
+ width: 100%;
+ background-color: $indigo-50;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ border-bottom: 1px solid darken($gray-normal, 8%);
+ }
+}
+
+.popover .feature-highlight-popover-content {
+ display: block;
+}
+
+.feature-highlight-popover {
+ padding: 0;
+
+ .popover-content {
+ padding: 0;
+ }
+}
+
+.feature-highlight-popover-sub-content {
+ padding: 9px 14px;
+}
+
+@include keyframes(pulse-highlight) {
+ 0% {
+ box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
+ }
+
+ 70% {
+ box-shadow: 0 0 0 10px transparent;
+ }
+
+ 100% {
+ box-shadow: 0 0 0 0 transparent;
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index a39927eb0df..c5c890b835f 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -267,14 +267,23 @@
// TODO: change global style
.ajax-project-dropdown,
+body[data-page="projects:edit"] #select2-drop,
body[data-page="projects:new"] #select2-drop,
+body[data-page="projects:merge_requests:edit"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop {
+ border: 1px solid $dropdown-border-color;
+ border-radius: $border-radius-base;
color: $gl-text-color;
}
+ &.select2-drop-above {
+ border-top: none;
+ margin-top: -4px;
+ }
+
.select2-results {
.select2-no-results,
.select2-searching,
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 0213e7aa9d9..e8ca5cedaee 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -143,8 +143,12 @@ ul.related-merge-requests > li {
}
}
-.issue-form .select2-container {
- width: 250px !important;
+.issue-form {
+ @include new-style-dropdown;
+
+ .select2-container {
+ width: 250px !important;
+ }
}
.issues-footer {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 8932cff22a8..5d7c85b16ef 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -23,6 +23,8 @@
.new-note,
.note-edit-form {
.note-form-actions {
+ @include new-style-dropdown;
+
position: relative;
margin: $gl-padding 0 0;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 19caefa1961..dd600a27545 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -800,8 +800,10 @@ pre.light-well {
}
}
-.new_protected_branch,
+.new-protected-branch,
.new-protected-tag {
+ @include new-style-dropdown;
+
label {
margin-top: 6px;
font-weight: $gl-font-weight-normal;
@@ -821,19 +823,9 @@ pre.light-well {
.protected-branches-list,
.protected-tags-list {
- margin-bottom: 30px;
-
- a {
- color: $gl-text-color;
-
- &:hover {
- color: $gl-link-color;
- }
+ @include new-style-dropdown;
- &.is-active {
- font-weight: $gl-font-weight-bold;
- }
- }
+ margin-bottom: 30px;
.settings-message {
margin: 0;
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index a6282187d34..ef9cdd058a9 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -305,14 +305,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true'
end
- def issuable_state_scope(issuable)
- if issuable.respond_to?(:merged?) && issuable.merged?
- :merged
- else
- issuable.open? ? :opened : :closed
- end
- end
-
def issuable_templates(issuable)
@issuable_templates ||=
case issuable
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 853ce827061..3d0fdce6a43 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -47,13 +47,6 @@ module IssuesHelper
end
end
- def bulk_update_milestone_options
- milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
- milestones.unshift(Milestone::None)
-
- options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
- end
-
def milestone_options(object)
milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed?
@@ -93,14 +86,6 @@ module IssuesHelper
return 'hidden' if issue.closed? == closed
end
- def merge_requests_sentence(merge_requests)
- # Sorting based on the `!123` or `group/project!123` reference will sort
- # local merge requests first.
- merge_requests.map do |merge_request|
- merge_request.to_reference(@project)
- end.sort.to_sentence(last_word_connector: ', or ')
- end
-
def confidential_icon(issue)
icon('eye-slash') if issue.confidential?
end
@@ -148,18 +133,6 @@ module IssuesHelper
end.to_h
end
- def due_date_options
- options = [
- Issue::AnyDueDate,
- Issue::NoDueDate,
- Issue::DueThisWeek,
- Issue::DueThisMonth,
- Issue::Overdue
- ]
-
- options_from_collection_for_select(options, 'name', 'title', params[:due_date])
- end
-
def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
link_text = merge_request.to_reference
link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index ca9a350ea79..35d14b6e297 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -305,6 +305,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self)
end
+ def has_kubernetes_active?
+ project.kubernetes_service&.active?
+ end
+
def has_stage_seeds?
stage_seeds.any?
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7a817eedec2..724fb4ccef1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -957,13 +957,6 @@ class MergeRequest < ActiveRecord::Base
private
def write_ref
- target_project.repository.with_repo_branch_commit(
- source_project.repository, source_branch) do |commit|
- if commit
- target_project.repository.write_ref(ref_path, commit.sha)
- else
- raise Rugged::ReferenceError, 'source repository is empty'
- end
- end
+ target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index b9247fb535a..051c4c8e2ec 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -68,7 +68,6 @@ class Project < ActiveRecord::Base
acts_as_taggable
- attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5474c8eeb68..05f2f851162 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -20,7 +20,6 @@ class Repository
delegate :ref_name_for_sha, to: :raw_repository
- CommitError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError)
# Methods that cache data from the Git repository.
@@ -95,19 +94,6 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
end
- #
- # Git repository can contains some hidden refs like:
- # /refs/notes/*
- # /refs/git-as-svn/*
- # /refs/pulls/*
- # This refs by default not visible in project page and not cloned to client side.
- #
- # This method return true if repository contains some content visible in project page.
- #
- def has_visible_content?
- branch_count > 0
- end
-
def commit(ref = 'HEAD')
return nil unless exists?
@@ -184,7 +170,7 @@ class Repository
return false unless newrev
- GitOperationService.new(user, self).add_branch(branch_name, newrev)
+ Gitlab::Git::OperationService.new(user, raw_repository).add_branch(branch_name, newrev)
after_create_branch
find_branch(branch_name)
@@ -196,7 +182,7 @@ class Repository
return false unless newrev
- GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
+ Gitlab::Git::OperationService.new(user, raw_repository).add_tag(tag_name, newrev, options)
find_tag(tag_name)
end
@@ -205,7 +191,7 @@ class Repository
before_remove_branch
branch = find_branch(branch_name)
- GitOperationService.new(user, self).rm_branch(branch)
+ Gitlab::Git::OperationService.new(user, raw_repository).rm_branch(branch)
after_remove_branch
true
@@ -215,7 +201,7 @@ class Repository
before_remove_tag
tag = find_tag(tag_name)
- GitOperationService.new(user, self).rm_tag(tag)
+ Gitlab::Git::OperationService.new(user, raw_repository).rm_tag(tag)
after_remove_tag
true
@@ -784,16 +770,30 @@ class Repository
multi_action(**options)
end
+ def with_branch(user, *args)
+ result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
+ yield start_commit
+ end
+
+ newrev, should_run_after_create, should_run_after_create_branch = result
+
+ after_create if should_run_after_create
+ after_create_branch if should_run_after_create_branch
+
+ newrev
+ end
+
# rubocop:disable Metrics/ParameterLists
def multi_action(
user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
branch_name,
start_branch_name: start_branch_name,
- start_project: start_project) do |start_commit|
+ start_repository: start_project.repository.raw_repository) do |start_commit|
index = Gitlab::Git::Index.new(raw_repository)
@@ -846,7 +846,8 @@ class Repository
end
def merge(user, source, merge_request, options = {})
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
merge_request.target_branch) do |start_commit|
our_commit = start_commit.sha
their_commit = source
@@ -866,17 +867,18 @@ class Repository
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
- rescue Repository::CommitError # when merge_index.conflicts?
+ rescue Gitlab::Git::CommitError # when merge_index.conflicts?
false
end
def revert(
user, commit, branch_name,
start_branch_name: nil, start_project: project)
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
branch_name,
start_branch_name: start_branch_name,
- start_project: start_project) do |start_commit|
+ start_repository: start_project.repository.raw_repository) do |start_commit|
revert_tree_id = check_revert_content(commit, start_commit.sha)
unless revert_tree_id
@@ -896,10 +898,11 @@ class Repository
def cherry_pick(
user, commit, branch_name,
start_branch_name: nil, start_project: project)
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
branch_name,
start_branch_name: start_branch_name,
- start_project: start_project) do |start_commit|
+ start_repository: start_project.repository.raw_repository) do |start_commit|
cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
unless cherry_pick_tree_id
@@ -921,7 +924,7 @@ class Repository
end
def resolve_conflicts(user, branch_name, params)
- GitOperationService.new(user, self).with_branch(branch_name) do
+ with_branch(user, branch_name) do
committer = user_to_committer(user)
create_commit(params.merge(author: committer, committer: committer))
@@ -1011,25 +1014,6 @@ class Repository
run_git(args).first.lines.map(&:strip)
end
- def with_repo_branch_commit(start_repository, start_branch_name)
- return yield nil if start_repository.empty_repo?
-
- if start_repository == self
- yield commit(start_branch_name)
- else
- sha = start_repository.commit(start_branch_name).sha
-
- if branch_commit = commit(sha)
- yield branch_commit
- else
- with_repo_tmp_commit(
- start_repository, start_branch_name, sha) do |tmp_commit|
- yield tmp_commit
- end
- end
- end
- end
-
def add_remote(name, url)
raw_repository.remote_add(name, url)
rescue Rugged::ConfigError
@@ -1047,14 +1031,12 @@ class Repository
gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags)
end
- def fetch_ref(source_path, source_ref, target_ref)
- args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
- message, status = run_git(args)
-
- # Make sure ref was created, and raise Rugged::ReferenceError when not
- raise Rugged::ReferenceError, message if status != 0
+ def fetch_source_branch(source_repository, source_branch, local_ref)
+ raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
+ end
- target_ref
+ def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
+ raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
end
def create_ref(ref, ref_path)
@@ -1135,12 +1117,6 @@ class Repository
private
- def run_git(args)
- circuit_breaker.perform do
- Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo)
- end
- end
-
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
@@ -1236,16 +1212,4 @@ class Repository
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) }
end
-
- def with_repo_tmp_commit(start_repository, start_branch_name, sha)
- tmp_ref = fetch_ref(
- start_repository.path_to_repo,
- "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
- "refs/tmp/#{SecureRandom.hex}/head"
- )
-
- yield commit(sha)
- ensure
- delete_refs(tmp_ref) if tmp_ref
- end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 68ec93a3ec5..9d48c82e861 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -644,11 +644,6 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count
end
- def projects_limit_percent
- return 100 if projects_limit.zero?
- (personal_projects.count.to_f / projects_limit) * 100
- end
-
def recent_push(project_ids = nil)
# Get push events not earlier than 2 hours ago
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
@@ -666,10 +661,6 @@ class User < ActiveRecord::Base
end
end
- def projects_sorted_by_activity
- authorized_projects.sorted_by_activity
- end
-
def several_namespaces?
owned_groups.any? || masters_groups.any?
end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index dbd0b9ef43a..f96f2931508 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -17,7 +17,7 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
- rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex
+ rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index a5ae4927412..53f16a236d2 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -11,26 +11,8 @@ class CompareService
end
def execute(target_project, target_branch, straight: false)
- # If compare with other project we need to fetch ref first
- target_project.repository.with_repo_branch_commit(
- start_project.repository,
- start_branch_name) do |commit|
- break unless commit
+ raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight)
- compare(commit.sha, target_project, target_branch, straight: straight)
- end
- end
-
- private
-
- def compare(source_sha, target_project, target_branch, straight:)
- raw_compare = Gitlab::Git::Compare.new(
- target_project.repository.raw_repository,
- target_branch,
- source_sha,
- straight: straight
- )
-
- Compare.new(raw_compare, target_project, straight: straight)
+ Compare.new(raw_compare, target_project, straight: straight) if raw_compare
end
end
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
deleted file mode 100644
index 6b7a56e6922..00000000000
--- a/app/services/git_operation_service.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-class GitOperationService
- attr_reader :committer, :repository
-
- def initialize(committer, new_repository)
- committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User)
- @committer = committer
-
- @repository = new_repository
- end
-
- def add_branch(branch_name, newrev)
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- oldrev = Gitlab::Git::BLANK_SHA
-
- update_ref_in_hooks(ref, newrev, oldrev)
- end
-
- def rm_branch(branch)
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
- oldrev = branch.target
- newrev = Gitlab::Git::BLANK_SHA
-
- update_ref_in_hooks(ref, newrev, oldrev)
- end
-
- def add_tag(tag_name, newrev, options = {})
- ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
- oldrev = Gitlab::Git::BLANK_SHA
-
- with_hooks(ref, newrev, oldrev) do |service|
- # We want to pass the OID of the tag object to the hooks. For an
- # annotated tag we don't know that OID until after the tag object
- # (raw_tag) is created in the repository. That is why we have to
- # update the value after creating the tag object. Only the
- # "post-receive" hook will receive the correct value in this case.
- raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
- service.newrev = raw_tag.target_id
- end
- end
-
- def rm_tag(tag)
- ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
- oldrev = tag.target
- newrev = Gitlab::Git::BLANK_SHA
-
- update_ref_in_hooks(ref, newrev, oldrev) do
- repository.rugged.tags.delete(tag_name)
- end
- end
-
- # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
- # it would be created from `start_branch_name`.
- # If `start_project` is passed, and the branch doesn't exist,
- # it would try to find the commits from it instead of current repository.
- def with_branch(
- branch_name,
- start_branch_name: nil,
- start_project: repository.project,
- &block)
-
- start_repository = start_project.repository
- start_branch_name = nil if start_repository.empty_repo?
-
- if start_branch_name && !start_repository.branch_exists?(start_branch_name)
- raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}"
- end
-
- update_branch_with_hooks(branch_name) do
- repository.with_repo_branch_commit(
- start_repository,
- start_branch_name || branch_name,
- &block)
- end
- end
-
- private
-
- def update_branch_with_hooks(branch_name)
- update_autocrlf_option
-
- was_empty = repository.empty?
-
- # Make commit
- newrev = yield
-
- unless newrev
- raise Repository::CommitError.new('Failed to create commit')
- end
-
- branch = repository.find_branch(branch_name)
- oldrev = find_oldrev_from_branch(newrev, branch)
-
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- update_ref_in_hooks(ref, newrev, oldrev)
-
- # If repo was empty expire cache
- repository.after_create if was_empty
- repository.after_create_branch if
- was_empty || Gitlab::Git.blank_ref?(oldrev)
-
- newrev
- end
-
- def find_oldrev_from_branch(newrev, branch)
- return Gitlab::Git::BLANK_SHA unless branch
-
- oldrev = branch.target
-
- if oldrev == repository.rugged.merge_base(newrev, branch.target)
- oldrev
- else
- raise Repository::CommitError.new('Branch diverged')
- end
- end
-
- def update_ref_in_hooks(ref, newrev, oldrev)
- with_hooks(ref, newrev, oldrev) do
- update_ref(ref, newrev, oldrev)
- end
- end
-
- def with_hooks(ref, newrev, oldrev)
- Gitlab::Git::HooksService.new.execute(
- committer,
- repository,
- oldrev,
- newrev,
- ref) do |service|
-
- yield(service)
- end
- end
-
- # Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites.
- def update_ref(ref, newrev, oldrev)
- # We use 'git update-ref' because libgit2/rugged currently does not
- # offer 'compare and swap' ref updates. Without compare-and-swap we can
- # (and have!) accidentally reset the ref to an earlier state, clobbering
- # commits. See also https://github.com/libgit2/libgit2/issues/1534.
- command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
- _, status = Gitlab::Popen.popen(
- command,
- repository.path_to_repo) do |stdin|
- stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
- end
-
- unless status.zero?
- raise Repository::CommitError.new(
- "Could not update branch #{Gitlab::Git.branch_name(ref)}." \
- " Please refresh and try again.")
- end
- end
-
- def update_autocrlf_option
- if repository.raw_repository.autocrlf != :input
- repository.raw_repository.autocrlf = :input
- end
- end
-end
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index c91602fcff7..30bf1384b22 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -22,10 +22,10 @@
%b Tag list:
= build[:tag_list].to_a.join(", ")
%br
- %b Refs only:
+ %b Only policy:
= @jobs[build[:name].to_sym][:only].to_a.join(", ")
%br
- %b Refs except:
+ %b Except policy:
= @jobs[build[:name].to_sym][:except].to_a.join(", ")
%br
%b Environment:
diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg
new file mode 100644
index 00000000000..1522c9d51c9
--- /dev/null
+++ b/app/views/feature_highlight/_issue_boards.svg
@@ -0,0 +1,98 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
+ <filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox">
+ <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
+ </filter>
+ <path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
+ <filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
+ <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
+ </filter>
+ <path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
+ <path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
+ <filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
+ <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
+ </filter>
+ <path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
+ <filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
+ <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
+ </filter>
+ <path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
+ <filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
+ <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
+ </filter>
+ <path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
+ <filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
+ <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
+ </filter>
+ <path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
+ <filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
+ <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
+ </filter>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/>
+ <g transform="translate(11 23)">
+ <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
+ <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
+ <g transform="translate(5 10)">
+ <use fill="black" filter="url(#a)" xlink:href="#b"/>
+ <use fill="#F9F9F9" xlink:href="#b"/>
+ </g>
+ <g transform="translate(5 42)">
+ <use fill="black" filter="url(#c)" xlink:href="#d"/>
+ <use fill="#FEF0E8" xlink:href="#d"/>
+ <path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
+ <path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
+ <path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/>
+ </g>
+ </g>
+ <path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/>
+ <g transform="translate(145 28)">
+ <mask id="f" fill="white">
+ <use xlink:href="#e"/>
+ </mask>
+ <use fill="#FFFFFF" xlink:href="#e"/>
+ <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/>
+ <g transform="translate(5 10)">
+ <use fill="black" filter="url(#g)" xlink:href="#h"/>
+ <use fill="#F9F9F9" xlink:href="#h"/>
+ </g>
+ <g transform="translate(5 42)">
+ <use fill="black" filter="url(#i)" xlink:href="#j"/>
+ <use fill="#FEF0E8" xlink:href="#j"/>
+ <path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/>
+ <path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
+ <path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/>
+ </g>
+ </g>
+ <path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/>
+ <g transform="translate(78 16)">
+ <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
+ <g transform="translate(5 10)">
+ <use fill="black" filter="url(#k)" xlink:href="#l"/>
+ <use fill="#EFEDF8" xlink:href="#l"/>
+ <path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
+ <path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
+ <path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
+ </g>
+ <g transform="translate(5 42)">
+ <use fill="black" filter="url(#m)" xlink:href="#n"/>
+ <use fill="#F9F9F9" xlink:href="#n"/>
+ </g>
+ <g transform="translate(5 74)">
+ <rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/>
+ <use fill="black" filter="url(#o)" xlink:href="#p"/>
+ <use fill="#F9F9F9" xlink:href="#p"/>
+ </g>
+ <path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index f5361c7af0c..760c4c97c33 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -99,6 +99,20 @@
= link_to project_boards_path(@project), title: 'Board' do
%span
Board
+ .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } }
+ .feature-highlight-popover-content
+ = render 'feature_highlight/issue_boards.svg'
+ .feature-highlight-popover-sub-content
+ %span= _('Use')
+ = link_to 'Issue Boards', project_boards_path(@project)
+ %span= _('to create customized software development workflows like')
+ %strong= _('Scrum')
+ %span= _('or')
+ %strong= _('Kanban')
+ %hr
+ %button.btn-link.dismiss-feature-highlight{ type: 'button' }
+ %span= _("Got it! Don't show this again")
+ = custom_icon('thumbs_up')
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do
diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg
new file mode 100644
index 00000000000..7267462418e
--- /dev/null
+++ b/app/views/shared/icons/_thumbs_up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg>