diff options
author | Connor Shea <connor.james.shea@gmail.com> | 2016-08-16 16:59:19 -0600 |
---|---|---|
committer | Connor Shea <connor.james.shea@gmail.com> | 2016-08-16 16:59:19 -0600 |
commit | 1d3aa59f99a72f613b84286eef948dfbad20925e (patch) | |
tree | 0061000ff2226f3bc4e88ce8133a65403de9f37e /app | |
parent | 1e7cbe0b05ddd9c72987730323e63d612d429ab9 (diff) | |
parent | 76aa85cc8023e1a8176f0b783a52154f98a5be8f (diff) | |
download | gitlab-ce-1d3aa59f99a72f613b84286eef948dfbad20925e.tar.gz |
Merge branch 'master' into diff-line-comment-vuejs
Diffstat (limited to 'app')
91 files changed, 1032 insertions, 295 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 49c2ac0dac3..84b292e59c6 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -9,10 +9,11 @@ licensePath: "/api/:version/licenses/:key", gitignorePath: "/api/:version/gitignores/:key", gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key", + issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", + group: function(group_id, callback) { - var url; - url = Api.buildUrl(Api.groupPath); - url = url.replace(':id', group_id); + var url = Api.buildUrl(Api.groupPath) + .replace(':id', group_id); return $.ajax({ url: url, data: { @@ -24,8 +25,7 @@ }); }, groups: function(query, skip_ldap, callback) { - var url; - url = Api.buildUrl(Api.groupsPath); + var url = Api.buildUrl(Api.groupsPath); return $.ajax({ url: url, data: { @@ -39,8 +39,7 @@ }); }, namespaces: function(query, callback) { - var url; - url = Api.buildUrl(Api.namespacesPath); + var url = Api.buildUrl(Api.namespacesPath); return $.ajax({ url: url, data: { @@ -54,8 +53,7 @@ }); }, projects: function(query, order, callback) { - var url; - url = Api.buildUrl(Api.projectsPath); + var url = Api.buildUrl(Api.projectsPath); return $.ajax({ url: url, data: { @@ -70,9 +68,8 @@ }); }, newLabel: function(project_id, data, callback) { - var url; - url = Api.buildUrl(Api.labelsPath); - url = url.replace(':id', project_id); + var url = Api.buildUrl(Api.labelsPath) + .replace(':id', project_id); data.private_token = gon.api_token; return $.ajax({ url: url, @@ -86,9 +83,8 @@ }); }, groupProjects: function(group_id, query, callback) { - var url; - url = Api.buildUrl(Api.groupProjectsPath); - url = url.replace(':id', group_id); + var url = Api.buildUrl(Api.groupProjectsPath) + .replace(':id', group_id); return $.ajax({ url: url, data: { @@ -102,8 +98,8 @@ }); }, licenseText: function(key, data, callback) { - var url; - url = Api.buildUrl(Api.licensePath).replace(':key', key); + var url = Api.buildUrl(Api.licensePath) + .replace(':key', key); return $.ajax({ url: url, data: data @@ -112,19 +108,32 @@ }); }, gitignoreText: function(key, callback) { - var url; - url = Api.buildUrl(Api.gitignorePath).replace(':key', key); + var url = Api.buildUrl(Api.gitignorePath) + .replace(':key', key); return $.get(url, function(gitignore) { return callback(gitignore); }); }, gitlabCiYml: function(key, callback) { - var url; - url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); + var url = Api.buildUrl(Api.gitlabCiYmlPath) + .replace(':key', key); return $.get(url, function(file) { return callback(file); }); }, + issueTemplate: function(namespacePath, projectPath, key, type, callback) { + var url = Api.buildUrl(Api.issuableTemplatePath) + .replace(':key', key) + .replace(':type', type) + .replace(':project_path', projectPath) + .replace(':namespace_path', namespacePath); + $.ajax({ + url: url, + dataType: 'json' + }).done(function(file) { + callback(null, file); + }).error(callback); + }, buildUrl: function(url) { if (gon.relative_url_root != null) { url = gon.relative_url_root + url; diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cc1042c92b8..4b8e687f3d4 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -41,6 +41,7 @@ /*= require date.format */ /*= require_directory ./behaviors */ /*= require_directory ./blob */ +/*= require_directory ./templates */ /*= require_directory ./commit */ /*= require_directory ./extensions */ /*= require_directory ./lib/utils */ diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 2cf0a6631b8..b0a37ef0e0a 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -9,6 +9,7 @@ } this.onClick = bind(this.onClick, this); this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name'); + this.dropdownIcon = $('.fa-chevron-down', this.dropdown); this.buildDropdown(); this.bindEvents(); this.onFilenameUpdate(); @@ -60,11 +61,26 @@ return this.requestFile(item); }; - TemplateSelector.prototype.requestFile = function(item) {}; + TemplateSelector.prototype.requestFile = function(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + }; - TemplateSelector.prototype.requestFileSuccess = function(file) { + TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) { this.editor.setValue(file.content, 1); - return this.editor.focus(); + if (!skipFocus) this.editor.focus(); + }; + + TemplateSelector.prototype.startLoadingSpinner = function() { + this.dropdownIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + }; + + TemplateSelector.prototype.stopLoadingSpinner = function() { + this.dropdownIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); }; return TemplateSelector; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 3946e861976..7160fa71ce5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -55,6 +55,7 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.issue-form')); new IssuableForm($('.issue-form')); + new IssuableTemplateSelectors(); break; case 'projects:merge_requests:new': case 'projects:merge_requests:edit': @@ -62,6 +63,7 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); + new IssuableTemplateSelectors(); break; case 'projects:tags:new': new ZenMode(); diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js index f27f1bad1f7..d0305c6c6a1 100644 --- a/app/assets/javascripts/issuable.js +++ b/app/assets/javascripts/issuable.js @@ -5,13 +5,10 @@ this.Issuable = { init: function() { - if (!issuable_created) { - issuable_created = true; - Issuable.initTemplates(); - Issuable.initSearch(); - Issuable.initChecks(); - return Issuable.initLabelFilterRemove(); - } + Issuable.initTemplates(); + Issuable.initSearch(); + Issuable.initChecks(); + return Issuable.initLabelFilterRemove(); }, initTemplates: function() { return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index 00e20a03b04..2efca2414dc 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -44,8 +44,8 @@ // Enable submit button const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); - const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); + const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ this.$form.find('input[type="submit"]').removeAttr('disabled'); diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 index 8d42e268ebc..a59fcbfa082 100644 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -39,12 +39,14 @@ _method: 'PATCH', id: this.$wrap.data('banchId'), protected_branch: { - merge_access_level_attributes: { + merge_access_levels_attributes: [{ + id: this.$allowedToMergeDropdown.data('access-level-id'), access_level: $allowedToMergeInput.val() - }, - push_access_level_attributes: { + }], + push_access_levels_attributes: [{ + id: this.$allowedToPushDropdown.data('access-level-id'), access_level: $allowedToPushInput.val() - } + }] } }, success: () => { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 new file mode 100644 index 00000000000..c32ddf80219 --- /dev/null +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -0,0 +1,51 @@ +/*= require ../blob/template_selector */ + +((global) => { + class IssuableTemplateSelector extends TemplateSelector { + constructor(...args) { + super(...args); + this.projectPath = this.dropdown.data('project-path'); + this.namespacePath = this.dropdown.data('namespace-path'); + this.issuableType = this.wrapper.data('issuable-type'); + this.titleInput = $(`#${this.issuableType}_title`); + + let initialQuery = { + name: this.dropdown.data('selected') + }; + + if (initialQuery.name) this.requestFile(initialQuery); + + $('.reset-template', this.dropdown.parent()).on('click', () => { + if (this.currentTemplate) this.setInputValueToTemplateContent(); + }); + } + + requestFile(query) { + this.startLoadingSpinner(); + Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => { + this.currentTemplate = currentTemplate; + if (err) return; // Error handled by global AJAX error handler + this.stopLoadingSpinner(); + this.setInputValueToTemplateContent(); + }); + return; + } + + setInputValueToTemplateContent() { + // `this.requestFileSuccess` sets the value of the description input field + // to the content of the template selected. + if (this.titleInput.val() === '') { + // If the title has not yet been set, focus the title input and + // skip focusing the description input by setting `true` as the 2nd + // argument to `requestFileSuccess`. + this.requestFileSuccess(this.currentTemplate, true); + this.titleInput.focus(); + } else { + this.requestFileSuccess(this.currentTemplate); + } + return; + } + } + + global.IssuableTemplateSelector = IssuableTemplateSelector; +})(window); diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 new file mode 100644 index 00000000000..bd8cdde033e --- /dev/null +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -0,0 +1,29 @@ +((global) => { + class IssuableTemplateSelectors { + constructor(opts = {}) { + this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector'); + this.editor = opts.editor || this.initEditor(); + + this.$dropdowns.each((i, dropdown) => { + let $dropdown = $(dropdown); + new IssuableTemplateSelector({ + pattern: /(\.md)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-issuable-selector-wrap'), + dropdown: $dropdown, + editor: this.editor + }); + }); + } + + initEditor() { + let editor = $('.markdown-area'); + // Proxy ace-editor's .setValue to jQuery's .val + editor.setValue = editor.val; + editor.getValue = editor.val; + return editor; + } + } + + global.IssuableTemplateSelectors = IssuableTemplateSelectors; +})(window); diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 473530cf094..f1fe1697d30 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -164,6 +164,10 @@ @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); } + &.btn-spam { + @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light); + } + &.btn-danger, &.btn-remove, &.btn-red { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e8eafa15899..f1635a53763 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -56,9 +56,13 @@ position: absolute; top: 50%; right: 6px; - margin-top: -4px; + margin-top: -6px; color: $dropdown-toggle-icon-color; font-size: 10px; + &.fa-spinner { + font-size: 16px; + margin-top: -8px; + } } &:hover, { @@ -406,6 +410,7 @@ font-size: 14px; a { + cursor: pointer; padding-left: 10px; } } diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 7cf4d4fba42..07c8874bf03 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -6,11 +6,11 @@ table-layout: fixed; pre { - padding: 10px; + padding: 10px 0; border: none; border-radius: 0; font-family: $monospace_font; - font-size: $code_font_size !important; + font-size: $code_font_size; line-height: $code_line_height !important; margin: 0; overflow: auto; @@ -20,13 +20,20 @@ border-left: 1px solid; code { + display: inline-block; + min-width: 100%; font-family: $monospace_font; - white-space: pre; + white-space: normal; word-wrap: normal; padding: 0; .line { - display: inline-block; + display: block; + width: 100%; + min-height: 19px; + padding-left: 10px; + padding-right: 10px; + white-space: pre; } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 7a50bc9c832..46c4a11aa2e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -395,3 +395,12 @@ display: inline-block; line-height: 18px; } + +.js-issuable-selector-wrap { + .js-issuable-selector { + width: 100%; + } + @media (max-width: $screen-sm-max) { + margin-bottom: $gl-padding; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 0a661e529f0..b4636269518 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -69,6 +69,10 @@ &.ci-success { color: $gl-success; + + a.environment { + color: inherit; + } } &.ci-success_with_warnings { @@ -126,7 +130,6 @@ &.has-conflicts .fa-exclamation-triangle { color: $gl-warning; } - } p:last-child { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index cf9aa02600d..27dc2b2a1fa 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -99,7 +99,7 @@ margin-left: auto; margin-right: auto; margin-bottom: 15px; - max-width: 480px; + max-width: 700px; > p { margin-bottom: 0; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index cf16d070cfe..0340526a53a 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -20,10 +20,43 @@ } } -.todo { +.todos-list > .todo { + // workaround because we cannot use border-colapse + border-top: 1px solid transparent; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + &:hover { + background-color: $row-hover; + border-color: $row-hover-border; cursor: pointer; } + + // overwrite border style of .content-list + &:last-child { + border-bottom: 1px solid transparent; + + &:hover { + border-color: $row-hover-border; + } + } + + .todo-actions { + display: -webkit-flex; + display: flex; + -webkit-justify-content: center; + justify-content: center; + -webkit-flex-direction: column; + flex-direction: column; + margin-left: 10px; + } + + .todo-item { + -webkit-flex: auto; + flex: auto; + } } .todo-item { @@ -43,8 +76,6 @@ } .todo-body { - margin-right: 174px; - .todo-note { word-wrap: break-word; @@ -90,6 +121,12 @@ } @media (max-width: $screen-xs-max) { + .todo { + .avatar { + display: none; + } + } + .todo-item { .todo-title { white-space: normal; @@ -98,10 +135,6 @@ margin-bottom: 10px; } - .avatar { - display: none; - } - .todo-body { margin: 0; border-left: 2px solid #ddd; diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index 3a2f0185315..2abfa22712d 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController head :ok end end + + def mark_as_ham + spam_log = SpamLog.find(params[:id]) + + if HamService.new(spam_log).mark_as_ham! + redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.' + else + redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.' + end + end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index d828d163c28..e1641ba6265 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,5 +1,6 @@ class AutocompleteController < ApplicationController skip_before_action :authenticate_user!, only: [:users] + before_action :load_project, only: [:users] before_action :find_users, only: [:users] def users @@ -55,11 +56,8 @@ class AutocompleteController < ApplicationController def find_users @users = - if params[:project_id].present? - project = Project.find(params[:project_id]) - return render_404 unless can?(current_user, :read_project, project) - - project.team.users + if @project + @project.team.users elsif params[:group_id].present? group = Group.find(params[:group_id]) return render_404 unless can?(current_user, :read_group, group) @@ -71,4 +69,14 @@ class AutocompleteController < ApplicationController User.none end end + + def load_project + @project ||= begin + if params[:project_id].present? + project = Project.find(params[:project_id]) + return render_404 unless can?(current_user, :read_project, project) + project + end + end + end end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 471d15af913..a69877edfd4 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -7,11 +7,16 @@ module ServiceParams :build_key, :server, :teamcity_url, :drone_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, - :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :build_events, :wiki_page_events, - :notify_only_broken_builds, :add_pusher, - :send_from_committer_email, :disable_diffs, :external_wiki_url, - :notify, :color, + # We're using `issues_events` and `merge_requests_events` + # in the view so we still need to explicitly state them + # here. `Service#event_names` would only give + # `issue_events` and `merge_request_events` (singular!) + # See app/helpers/services_helper.rb for how we + # make those event names plural as special case. + :issues_events, :merge_requests_events, + :notify_only_broken_builds, :notify_only_broken_pipelines, + :add_pusher, :send_from_committer_email, :disable_diffs, + :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, :jira_issue_transition_id] @@ -19,9 +24,7 @@ module ServiceParams FILTER_BLANK_PARAMS = [:password] def service_params - dynamic_params = [] - dynamic_params.concat(@service.event_channel_names) - + dynamic_params = @service.event_channel_names + @service.event_names service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) if service_params[:service].is_a?(Hash) diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb new file mode 100644 index 00000000000..29e243c66a3 --- /dev/null +++ b/app/controllers/concerns/spammable_actions.rb @@ -0,0 +1,25 @@ +module SpammableActions + extend ActiveSupport::Concern + + included do + before_action :authorize_submit_spammable!, only: :mark_as_spam + end + + def mark_as_spam + if SpamService.new(spammable).mark_as_spam! + redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully." + else + redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' + end + end + + private + + def spammable + raise NotImplementedError, "#{self.class} does not implement #{__method__}" + end + + def authorize_submit_spammable! + access_denied! unless current_user.admin? + end +end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 19a76a5b5d8..1243bb96d4d 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -37,8 +37,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController def todos_counts { - count: TodosFinder.new(current_user, state: :pending).execute.count, - done_count: TodosFinder.new(current_user, state: :done).execute.count + count: current_user.todos_pending_count, + done_count: current_user.todos_done_count } end end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 3ec173abcdb..7d0eff37635 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -1,5 +1,6 @@ class Import::GitlabProjectsController < Import::BaseController before_action :verify_gitlab_project_import_enabled + before_action :authenticate_admin! def new @namespace_id = project_params[:namespace_id] @@ -47,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController :path, :namespace_id, :file ) end + + def authenticate_admin! + render_404 unless current_user.is_admin? + end end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index d0f5071d2cc..6c25cd83a24 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -4,11 +4,24 @@ class Projects::BadgesController < Projects::ApplicationController before_action :no_cache_headers, except: [:index] def build - badge = Gitlab::Badge::Build.new(project, params[:ref]) + build_status = Gitlab::Badge::Build::Status + .new(project, params[:ref]) + render_badge build_status + end + + def coverage + coverage_report = Gitlab::Badge::Coverage::Report + .new(project, params[:ref], params[:job]) + + render_badge coverage_report + end + + private + + def render_badge(badge) respond_to do |format| format.html { render_404 } - format.svg do render 'badge', locals: { badge: badge.template } end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 19d051720e9..cdf9a04bacf 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController before_action :require_branch_head, only: [:edit, :update] before_action :editor_variables, except: [:show, :preview, :diff] before_action :validate_diff_params, only: :diff + before_action :set_last_commit_sha, only: [:edit, :update] def new commit unless @repository.empty? @@ -33,7 +34,6 @@ class Projects::BlobController < Projects::ApplicationController end def edit - @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha blob.load_all_data!(@repository) end @@ -55,6 +55,10 @@ class Projects::BlobController < Projects::ApplicationController create_commit(Files::UpdateService, success_path: after_edit_path, failure_view: :edit, failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) + + rescue Files::UpdateService::FileChangedError + @conflict = true + render :edit end def preview @@ -152,7 +156,8 @@ class Projects::BlobController < Projects::ApplicationController file_path: @file_path, commit_message: params[:commit_message], file_content: params[:content], - file_content_encoding: params[:encoding] + file_content_encoding: params[:encoding], + last_commit_sha: params[:last_commit_sha] } end @@ -161,4 +166,9 @@ class Projects::BlobController < Projects::ApplicationController render nothing: true end end + + def set_last_commit_sha + @last_commit_sha = Gitlab::Git::Commit. + last_for_path(@repository, @ref, @path).sha + end end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index a60027ff477..b5624046387 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController def hook_params params.require(:hook).permit( :build_events, + :pipeline_events, :enable_ssl_verification, :issues_events, :merge_requests_events, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 660e0eba06f..e9fb11e8f94 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions include ToggleAwardEmoji include IssuableCollections + include SpammableActions before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :module_enabled @@ -185,6 +186,7 @@ class Projects::IssuesController < Projects::ApplicationController alias_method :subscribable_resource, :issue alias_method :issuable, :issue alias_method :awardable, :issue + alias_method :spammable, :issue def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 75dd3648e45..9136633b87a 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -3,7 +3,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def show @ref = params[:ref] || @project.default_branch || 'master' - @build_badge = Gitlab::Badge::Build.new(@project, @ref).metadata + + @badges = [Gitlab::Badge::Build::Status, + Gitlab::Badge::Coverage::Report] + + @badges.map! do |badge| + badge.new(@project, @ref).metadata + end end def update diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index d28ec6e2eac..9a438d5512c 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -9,16 +9,16 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def index @protected_branch = @project.protected_branches.new - load_protected_branches_gon_variables + load_gon_index end def create - @protected_branch = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute + @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute if @protected_branch.persisted? redirect_to namespace_project_protected_branches_path(@project.namespace, @project) else load_protected_branches - load_protected_branches_gon_variables + load_gon_index render :index end end @@ -28,7 +28,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController end def update - @protected_branch = ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch) + @protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch) if @protected_branch.valid? respond_to do |format| @@ -58,17 +58,23 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def protected_branch_params params.require(:protected_branch).permit(:name, - merge_access_level_attributes: [:access_level], - push_access_level_attributes: [:access_level]) + merge_access_levels_attributes: [:access_level, :id], + push_access_levels_attributes: [:access_level, :id]) end def load_protected_branches @protected_branches = @project.protected_branches.order(:name).page(params[:page]) end - def load_protected_branches_gon_variables - gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }, - push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } }, - merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } }) + def access_levels_options + { + push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }, + merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } } + } + end + + def load_gon_index + params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } } + gon.push(params.merge(access_levels_options)) end end diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb new file mode 100644 index 00000000000..694b468c8d3 --- /dev/null +++ b/app/controllers/projects/templates_controller.rb @@ -0,0 +1,19 @@ +class Projects::TemplatesController < Projects::ApplicationController + before_action :authenticate_user!, :get_template_class + + def show + template = @template_type.find(params[:key], project) + + respond_to do |format| + format.json { render json: template.to_json } + end + end + + private + + def get_template_class + template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access + @template_type = template_types[params[:template_type]] + render json: [], status: 404 unless @template_type + end +end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 2f0a9659d15..c7911736812 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -1,6 +1,7 @@ class ProjectsFinder < UnionFinder - def execute(current_user = nil, options = {}) + def execute(current_user = nil, project_ids_relation = nil) segments = all_projects(current_user) + segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation find_union(segments, Project) end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index ff866c2faa5..4fe0070552e 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -27,9 +27,11 @@ class TodosFinder items = by_action_id(items) items = by_action(items) items = by_author(items) - items = by_project(items) items = by_state(items) items = by_type(items) + # Filtering by project HAS TO be the last because we use + # the project IDs yielded by the todos query thus far + items = by_project(items) items.reorder(id: :desc) end @@ -91,14 +93,9 @@ class TodosFinder @project end - def projects - return @projects if defined?(@projects) - - if project? - @projects = project - else - @projects = ProjectsFinder.new.execute(current_user) - end + def projects(items) + item_project_ids = items.reorder(nil).select(:project_id) + ProjectsFinder.new.execute(current_user, item_project_ids) end def type? @@ -136,8 +133,9 @@ class TodosFinder def by_project(items) if project? items = items.where(project: project) - elsif projects - items = items.merge(projects).joins(:project) + else + item_projects = projects(items) + items = items.merge(item_projects).joins(:project) end items diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 48c27828219..1cb5d847626 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -182,17 +182,42 @@ module BlobHelper } end + def selected_template(issuable) + templates = issuable_templates(issuable) + params[:issuable_template] if templates.include?(params[:issuable_template]) + end + + def can_add_template?(issuable) + names = issuable_templates(issuable) + names.empty? && can?(current_user, :push_code, @project) && !@project.private? + end + + def merge_request_template_names + @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project) + end + + def issue_template_names + @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project) + end + + def issuable_templates(issuable) + @issuable_templates ||= + if issuable.is_a?(Issue) + issue_template_names + elsif issuable.is_a?(MergeRequest) + merge_request_template_names + end + end + + def ref_project + @ref_project ||= @target_project || @project + end + def gitignore_names - @gitignore_names ||= - Gitlab::Template::Gitignore.categories.keys.map do |k| - [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }] - end.to_h + @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names end def gitlab_ci_ymls - @gitlab_ci_ymls ||= - Gitlab::Template::GitlabCiYml.categories.keys.map do |k| - [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }] - end.to_h + @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names end end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index e1c0b497550..8b138a8e69f 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -20,13 +20,19 @@ module SortingHelper end def projects_sort_options_hash - { + options = { sort_value_name => sort_title_name, sort_value_recently_updated => sort_title_recently_updated, sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, sort_value_oldest_created => sort_title_oldest_created, } + + if current_controller?('admin/projects') + options.merge!(sort_value_largest_repo => sort_title_largest_repo) + end + + options end def sort_title_priority diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index e3a208f826a..0465327060e 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -1,10 +1,10 @@ module TodosHelper def todos_pending_count - @todos_pending_count ||= TodosFinder.new(current_user, state: :pending).execute.count + @todos_pending_count ||= current_user.todos_pending_count end def todos_done_count - @todos_done_count ||= TodosFinder.new(current_user, state: :done).execute.count + @todos_done_count ||= current_user.todos_done_count end def todo_action_name(todo) diff --git a/app/models/blob.rb b/app/models/blob.rb index 0df2805e448..12cc5aaafba 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -3,6 +3,9 @@ class Blob < SimpleDelegator CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour + # The maximum size of an SVG that can be displayed. + MAXIMUM_SVG_SIZE = 2.megabytes + # Wrap a Gitlab::Git::Blob object, or return nil when given nil # # This method prevents the decorated object from evaluating to "truthy" when @@ -31,6 +34,10 @@ class Blob < SimpleDelegator text? && language && language.name == 'SVG' end + def size_within_svg_limits? + size <= MAXIMUM_SVG_SIZE + end + def video? UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.')) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3d6c6ea3209..4c84f4c21c5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -344,7 +344,7 @@ module Ci def execute_hooks return unless project - build_data = Gitlab::BuildDataBuilder.build(self) + build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) project.running_or_pending_build_count(force: true) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 8cfba92ae9b..130afeb724e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -19,6 +19,8 @@ module Ci after_save :keep_around_commits + delegate :stages, to: :statuses + state_machine :status, initial: :created do event :enqueue do transition created: :pending @@ -56,6 +58,10 @@ module Ci before_transition do |pipeline| pipeline.update_duration end + + after_transition do |pipeline, transition| + pipeline.execute_hooks unless transition.loopback? + end end # ref can't be HEAD or SHA, can only be branch/tag name @@ -243,8 +249,18 @@ module Ci self.duration = statuses.latest.duration end + def execute_hooks + data = pipeline_data + project.execute_hooks(data, :pipeline_hooks) + project.execute_services(data, :pipeline_hooks) + end + private + def pipeline_data + Gitlab::DataBuilder::Pipeline.build(self) + end + def latest_builds_status return 'failed' unless yaml_errors.blank? diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb new file mode 100644 index 00000000000..5a7b36070e7 --- /dev/null +++ b/app/models/concerns/protected_branch_access.rb @@ -0,0 +1,7 @@ +module ProtectedBranchAccess + extend ActiveSupport::Concern + + def humanize + self.class.human_access_levels[self.access_level] + end +end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 3b8e6df2da9..ce54fe5d3bf 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -1,9 +1,32 @@ module Spammable extend ActiveSupport::Concern + module ClassMethods + def attr_spammable(attr, options = {}) + spammable_attrs << [attr.to_s, options] + end + end + included do + has_one :user_agent_detail, as: :subject, dependent: :destroy + attr_accessor :spam + after_validation :check_for_spam, on: :create + + cattr_accessor :spammable_attrs, instance_accessor: false do + [] + end + + delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true + end + + def submittable_as_spam? + if user_agent_detail + user_agent_detail.submittable? + else + false + end end def spam? @@ -13,4 +36,33 @@ module Spammable def check_for_spam self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? end + + def spam_title + attr = self.class.spammable_attrs.find do |_, options| + options.fetch(:spam_title, false) + end + + public_send(attr.first) if attr && respond_to?(attr.first.to_sym) + end + + def spam_description + attr = self.class.spammable_attrs.find do |_, options| + options.fetch(:spam_description, false) + end + + public_send(attr.first) if attr && respond_to?(attr.first.to_sym) + end + + def spammable_text + result = self.class.spammable_attrs.map do |attr| + public_send(attr.first) + end + + result.reject(&:blank?).join("\n") + end + + # Override in Spammable if further checks are necessary + def check_for_spam? + true + end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1a7cd60817e..1e338889714 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -36,4 +36,10 @@ class Deployment < ActiveRecord::Base def manual_actions deployable.try(:other_actions) end + + def includes_commit?(commit) + return false unless commit + + project.repository.is_ancestor?(commit.id, sha) + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index baed106e8c8..75e6f869786 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -25,4 +25,10 @@ class Environment < ActiveRecord::Base def nullify_external_url self.external_url = nil if self.external_url.blank? end + + def includes_commit?(commit) + return false unless last_deployment + + last_deployment.includes_commit?(commit) + end end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index ba42a8eeb70..836a75b0608 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -5,5 +5,6 @@ class ProjectHook < WebHook scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :build_hooks, -> { where(build_events: true) } + scope :pipeline_hooks, -> { where(pipeline_events: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true) } end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 8b87b6c3d64..f365dee3141 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base default_value_for :merge_requests_events, false default_value_for :tag_push_events, false default_value_for :build_events, false + default_value_for :pipeline_events, false default_value_for :enable_ssl_verification, true scope :push_hooks, -> { where(push_events: true) } diff --git a/app/models/issue.rb b/app/models/issue.rb index d62ffb21467..788611305fe 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -36,6 +36,9 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + attr_spammable :title, spam_title: true + attr_spammable :description, spam_description: true + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -262,4 +265,9 @@ class Issue < ActiveRecord::Base def overdue? due_date.try(:past?) || false end + + # Only issues on public projects should be checked for spam + def check_for_spam? + project.public? + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index afff006065e..5bb48117851 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -610,6 +610,14 @@ class MergeRequest < ActiveRecord::Base !pipeline || pipeline.success? end + def environments + return unless diff_head_commit + + target_project.environments.select do |environment| + environment.includes_commit?(diff_head_commit) + end + end + def state_human_name if merged? "Merged" diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 5e166471077..fa66e5864b8 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -51,8 +51,7 @@ class BuildsEmailService < Service end def test_data(project = nil, user = nil) - build = project.builds.last - Gitlab::BuildDataBuilder.build(build) + Gitlab::DataBuilder::Build.build(project.builds.last) end def fields diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index ad19b7795da..5301f9fa0ff 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,7 +1,9 @@ class PivotaltrackerService < Service include HTTParty - prop_accessor :token + API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits' + + prop_accessor :token, :restrict_to_branch validates :token, presence: true, if: :activated? def title @@ -18,7 +20,17 @@ class PivotaltrackerService < Service def fields [ - { type: 'text', name: 'token', placeholder: '' } + { + type: 'text', + name: 'token', + placeholder: 'Pivotal Tracker API token.' + }, + { + type: 'text', + name: 'restrict_to_branch', + placeholder: 'Comma-separated list of branches which will be ' \ + 'automatically inspected. Leave blank to include all branches.' + } ] end @@ -28,8 +40,8 @@ class PivotaltrackerService < Service def execute(data) return unless supported_events.include?(data[:object_kind]) + return unless allowed_branch?(data[:ref]) - url = 'https://www.pivotaltracker.com/services/v5/source_commits' data[:commits].each do |commit| message = { 'source_commit' => { @@ -40,7 +52,7 @@ class PivotaltrackerService < Service } } PivotaltrackerService.post( - url, + API_ENDPOINT, body: message.to_json, headers: { 'Content-Type' => 'application/json', @@ -49,4 +61,15 @@ class PivotaltrackerService < Service ) end end + + private + + def allowed_branch?(ref) + return true unless ref.present? && restrict_to_branch.present? + + branch = Gitlab::Git.ref_name(ref) + allowed_branches = restrict_to_branch.split(',').map(&:strip) + + branch.present? && allowed_branches.include?(branch) + end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index a255710f577..46f70da2452 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -56,6 +56,10 @@ class ProjectWiki end end + def repository_exists? + !!repository.exists? + end + def empty? pages.empty? end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 226b3f54342..6240912a6e1 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -5,11 +5,14 @@ class ProtectedBranch < ActiveRecord::Base validates :name, presence: true validates :project, presence: true - has_one :merge_access_level, dependent: :destroy - has_one :push_access_level, dependent: :destroy + has_many :merge_access_levels, dependent: :destroy + has_many :push_access_levels, dependent: :destroy - accepts_nested_attributes_for :push_access_level - accepts_nested_attributes_for :merge_access_level + validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch." + validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch." + + accepts_nested_attributes_for :push_access_levels + accepts_nested_attributes_for :merge_access_levels def commit project.commit(self.name) diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index b1112ee737d..806b3ccd275 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,4 +1,6 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base + include ProtectedBranchAccess + belongs_to :protected_branch delegate :project, to: :protected_branch @@ -17,8 +19,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base project.team.max_member_access(user.id) >= access_level end - - def humanize - self.class.human_access_levels[self.access_level] - end end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 6a5e49cf453..92e9c51d883 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,4 +1,6 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base + include ProtectedBranchAccess + belongs_to :protected_branch delegate :project, to: :protected_branch @@ -20,8 +22,4 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base project.team.max_member_access(user.id) >= access_level end - - def humanize - self.class.human_access_levels[self.access_level] - end end diff --git a/app/models/service.rb b/app/models/service.rb index 40cd9b861f0..09b4717a523 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -36,6 +36,7 @@ class Service < ActiveRecord::Base scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } + scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } @@ -79,13 +80,17 @@ class Service < ActiveRecord::Base end def test_data(project, user) - Gitlab::PushDataBuilder.build_sample(project, user) + Gitlab::DataBuilder::Push.build_sample(project, user) end def event_channel_names [] end + def event_names + supported_events.map { |event| "#{event}_events" } + end + def event_field(event) nil end diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb index 12df68ef83b..3b8b9833565 100644 --- a/app/models/spam_log.rb +++ b/app/models/spam_log.rb @@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base user.block user.destroy end + + def text + [title, description].join("\n") + end end diff --git a/app/models/user.rb b/app/models/user.rb index 73368be7b1b..87a2d999843 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -809,13 +809,13 @@ class User < ActiveRecord::Base def todos_done_count(force: false) Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do - todos.done.count + TodosFinder.new(self, state: :done).execute.count end end def todos_pending_count(force: false) Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do - todos.pending.count + TodosFinder.new(self, state: :pending).execute.count end end diff --git a/app/models/user_agent_detail.rb b/app/models/user_agent_detail.rb new file mode 100644 index 00000000000..0949c6ef083 --- /dev/null +++ b/app/models/user_agent_detail.rb @@ -0,0 +1,9 @@ +class UserAgentDetail < ActiveRecord::Base + belongs_to :subject, polymorphic: true + + validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true + + def submittable? + !submitted? + end +end diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb new file mode 100644 index 00000000000..5c60addbe7c --- /dev/null +++ b/app/services/akismet_service.rb @@ -0,0 +1,79 @@ +class AkismetService + attr_accessor :owner, :text, :options + + def initialize(owner, text, options = {}) + @owner = owner + @text = text + @options = options + end + + def is_spam? + return false unless akismet_enabled? + + params = { + type: 'comment', + text: text, + created_at: DateTime.now, + author: owner.name, + author_email: owner.email, + referrer: options[:referrer], + } + + begin + is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params) + is_spam || is_blatant + rescue => e + Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check") + false + end + end + + def submit_ham + return false unless akismet_enabled? + + params = { + type: 'comment', + text: text, + author: owner.name, + author_email: owner.email + } + + begin + akismet_client.submit_ham(options[:ip_address], options[:user_agent], params) + true + rescue => e + Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") + false + end + end + + def submit_spam + return false unless akismet_enabled? + + params = { + type: 'comment', + text: text, + author: owner.name, + author_email: owner.email + } + + begin + akismet_client.submit_spam(options[:ip_address], options[:user_agent], params) + true + rescue => e + Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") + false + end + end + + private + + def akismet_client + @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key, + Gitlab.config.gitlab.url) + end + + def akismet_enabled? + current_application_settings.akismet_enabled + end +end diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb deleted file mode 100644 index 59a66fde47a..00000000000 --- a/app/services/create_spam_log_service.rb +++ /dev/null @@ -1,13 +0,0 @@ -class CreateSpamLogService < BaseService - def initialize(project, user, params) - super(project, user, params) - end - - def execute - spam_params = params.merge({ user_id: @current_user.id, - project_id: @project.id } ) - spam_log = SpamLog.new(spam_params) - spam_log.save - spam_log - end -end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 87f066edb6f..918eddaa53a 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -39,7 +39,12 @@ class DeleteBranchService < BaseService end def build_push_data(branch) - Gitlab::PushDataBuilder - .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + Gitlab::DataBuilder::Push.build( + project, + current_user, + branch.target.sha, + Gitlab::Git::BLANK_SHA, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", + []) end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index 32e0eed6b63..d0cb151a010 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -33,7 +33,12 @@ class DeleteTagService < BaseService end def build_push_data(tag) - Gitlab::PushDataBuilder - .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + Gitlab::DataBuilder::Push.build( + project, + current_user, + tag.target.sha, + Gitlab::Git::BLANK_SHA, + "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", + []) end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index c4a206f785e..ea94818713b 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -15,6 +15,7 @@ module Files else params[:file_content] end + @last_commit_sha = params[:last_commit_sha] # Validate parameters validate diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 8d2b5083179..4fc3b640799 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -2,11 +2,34 @@ require_relative "base_service" module Files class UpdateService < Files::BaseService + class FileChangedError < StandardError; end + def commit repository.update_file(current_user, @file_path, @file_content, branch: @target_branch, previous_path: @previous_path, message: @commit_message) end + + private + + def validate + super + + if file_has_changed? + raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.") + end + end + + def file_has_changed? + return false unless @last_commit_sha && last_commit + + @last_commit_sha != last_commit.sha + end + + def last_commit + @last_commit ||= Gitlab::Git::Commit. + last_for_path(@source_project.repository, @source_branch, @file_path) + end end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 6f521462cf3..78feb37aa2a 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -91,12 +91,12 @@ class GitPushService < BaseService params = { name: @project.default_branch, - push_access_level_attributes: { + push_access_levels_attributes: [{ access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - }, - merge_access_level_attributes: { + }], + merge_access_levels_attributes: [{ access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - } + }] } ProtectedBranches::CreateService.new(@project, current_user, params).execute @@ -138,13 +138,23 @@ class GitPushService < BaseService end def build_push_data - @push_data ||= Gitlab::PushDataBuilder. - build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits) + @push_data ||= Gitlab::DataBuilder::Push.build( + @project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + push_commits) end def build_push_data_system_hook - @push_data_system ||= Gitlab::PushDataBuilder. - build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], []) + @push_data_system ||= Gitlab::DataBuilder::Push.build( + @project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + []) end def push_to_existing_branch? diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index d2b52f16fa8..e6002b03b93 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -34,12 +34,24 @@ class GitTagPushService < BaseService end end - Gitlab::PushDataBuilder. - build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message) + Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + commits, + message) end def build_system_push_data - Gitlab::PushDataBuilder. - build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '') + Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + [], + '') end end diff --git a/app/services/ham_service.rb b/app/services/ham_service.rb new file mode 100644 index 00000000000..b0e1799b489 --- /dev/null +++ b/app/services/ham_service.rb @@ -0,0 +1,26 @@ +class HamService + attr_accessor :spam_log + + def initialize(spam_log) + @spam_log = spam_log + end + + def mark_as_ham! + if akismet.submit_ham + spam_log.update_attribute(:submitted_as_ham, true) + else + false + end + end + + private + + def akismet + @akismet ||= AkismetService.new( + spam_log.user, + spam_log.text, + ip_address: spam_log.source_ip, + user_agent: spam_log.user_agent + ) + end +end diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 5e2de2ccf64..65550ab8ec6 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -3,29 +3,34 @@ module Issues def execute filter_params label_params = params.delete(:label_ids) - request = params.delete(:request) - api = params.delete(:api) - issue = project.issues.new(params) - issue.author = params[:author] || current_user + @request = params.delete(:request) + @api = params.delete(:api) + @issue = project.issues.new(params) + @issue.author = params[:author] || current_user - issue.spam = spam_check_service.execute(request, api) + @issue.spam = spam_service.check(@api) - if issue.save - issue.update_attributes(label_ids: label_params) - notification_service.new_issue(issue, current_user) - todo_service.new_issue(issue, current_user) - event_service.open_issue(issue, current_user) - issue.create_cross_references!(current_user) - execute_hooks(issue, 'open') + if @issue.save + @issue.update_attributes(label_ids: label_params) + notification_service.new_issue(@issue, current_user) + todo_service.new_issue(@issue, current_user) + event_service.open_issue(@issue, current_user) + user_agent_detail_service.create + @issue.create_cross_references!(current_user) + execute_hooks(@issue, 'open') end - issue + @issue end private - def spam_check_service - SpamCheckService.new(project, current_user, params) + def spam_service + SpamService.new(@issue, @request) + end + + def user_agent_detail_service + UserAgentDetailService.new(@issue, @request) end end end diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 501fd135e16..08c1f72d65a 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -30,10 +30,21 @@ module MergeRequests end def get_branches(changes) + return [] if project.empty_repo? + return [] unless project.merge_requests_enabled + changes_list = Gitlab::ChangesList.new(changes) changes_list.map do |change| next unless Gitlab::Git.branch_ref?(change[:ref]) - Gitlab::Git.branch_name(change[:ref]) + + # Deleted branch + next if Gitlab::Git.blank_ref?(change[:newrev]) + + # Default branch + branch_name = Gitlab::Git.branch_name(change[:ref]) + next if branch_name == project.default_branch + + branch_name end.compact end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index 534c48aefff..e4cd3fc7833 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -16,7 +16,7 @@ module Notes end def hook_data - Gitlab::NoteDataBuilder.build(@note, @note.author) + Gitlab::DataBuilder::Note.build(@note, @note.author) end def execute_note_hooks diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index 6150a2a83c9..a84e335340d 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -5,23 +5,7 @@ module ProtectedBranches def execute raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) - protected_branch = project.protected_branches.new(params) - - ProtectedBranch.transaction do - protected_branch.save! - - if protected_branch.push_access_level.blank? - protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER) - end - - if protected_branch.merge_access_level.blank? - protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER) - end - end - - protected_branch - rescue ActiveRecord::RecordInvalid - protected_branch + project.protected_branches.create(params) end end end diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb deleted file mode 100644 index 7c3e692bde9..00000000000 --- a/app/services/spam_check_service.rb +++ /dev/null @@ -1,38 +0,0 @@ -class SpamCheckService < BaseService - include Gitlab::AkismetHelper - - attr_accessor :request, :api - - def execute(request, api) - @request, @api = request, api - return false unless request || check_for_spam?(project) - return false unless is_spam?(request.env, current_user, text) - - create_spam_log - - true - end - - private - - def text - [params[:title], params[:description]].reject(&:blank?).join("\n") - end - - def spam_log_attrs - { - user_id: current_user.id, - project_id: project.id, - title: params[:title], - description: params[:description], - source_ip: client_ip(request.env), - user_agent: user_agent(request.env), - noteable_type: 'Issue', - via_api: api - } - end - - def create_spam_log - CreateSpamLogService.new(project, current_user, spam_log_attrs).execute - end -end diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb new file mode 100644 index 00000000000..48903291799 --- /dev/null +++ b/app/services/spam_service.rb @@ -0,0 +1,78 @@ +class SpamService + attr_accessor :spammable, :request, :options + + def initialize(spammable, request = nil) + @spammable = spammable + @request = request + @options = {} + + if @request + @options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s + @options[:user_agent] = @request.env['HTTP_USER_AGENT'] + @options[:referrer] = @request.env['HTTP_REFERRER'] + else + @options[:ip_address] = @spammable.ip_address + @options[:user_agent] = @spammable.user_agent + end + end + + def check(api = false) + return false unless request && check_for_spam? + + return false unless akismet.is_spam? + + create_spam_log(api) + true + end + + def mark_as_spam! + return false unless spammable.submittable_as_spam? + + if akismet.submit_spam + spammable.user_agent_detail.update_attribute(:submitted, true) + else + false + end + end + + private + + def akismet + @akismet ||= AkismetService.new( + spammable_owner, + spammable.spammable_text, + options + ) + end + + def spammable_owner + @user ||= User.find(spammable_owner_id) + end + + def spammable_owner_id + @owner_id ||= + if spammable.respond_to?(:author_id) + spammable.author_id + elsif spammable.respond_to?(:creator_id) + spammable.creator_id + end + end + + def check_for_spam? + spammable.check_for_spam? + end + + def create_spam_log(api) + SpamLog.create( + { + user_id: spammable_owner_id, + title: spammable.spam_title, + description: spammable.spam_description, + source_ip: options[:ip_address], + user_agent: options[:user_agent], + noteable_type: spammable.class.to_s, + via_api: api + } + ) + end +end diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb index e85e58751e7..280c81f7d2d 100644 --- a/app/services/test_hook_service.rb +++ b/app/services/test_hook_service.rb @@ -1,6 +1,6 @@ class TestHookService def execute(hook, current_user) - data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user) + data = Gitlab::DataBuilder::Push.build_sample(hook.project, current_user) hook.execute(data, 'push_hooks') end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 6b48d68cccb..eb833dd82ac 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -144,8 +144,9 @@ class TodoService def mark_todos_as_done(todos, current_user) todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all) - todos.update_all(state: :done) + marked_todos = todos.update_all(state: :done) current_user.update_todos_count_cache + marked_todos end # When user marks an issue as todo diff --git a/app/services/user_agent_detail_service.rb b/app/services/user_agent_detail_service.rb new file mode 100644 index 00000000000..a1ee3df5fe1 --- /dev/null +++ b/app/services/user_agent_detail_service.rb @@ -0,0 +1,13 @@ +class UserAgentDetailService + attr_accessor :spammable, :request + + def initialize(spammable, request) + @spammable, @request = spammable, request + end + + def create + return unless request + + spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s) + end +end diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml index 8aea67f4497..4ce4eab8753 100644 --- a/app/views/admin/spam_logs/_spam_log.html.haml +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -24,6 +24,11 @@ = link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true), data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" %td + - if spam_log.submitted_as_ham? + .btn.btn-xs.disabled + Submitted as ham + - else + = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning' - if user && !user.blocked? = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" - else diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 98f302d2f93..b40395c74de 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,6 +1,7 @@ %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } + = author_avatar(todo, size: 40) + .todo-item.todo-block - = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' .todo-title.title - unless todo.build_failed? = todo_target_state_pill(todo) @@ -19,13 +20,13 @@ · #{time_ago_with_tooltip(todo.created_at)} - - if todo.pending? - .todo-actions.pull-right - = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do - Done - = icon('spinner spin') - .todo-body .todo-note .md = event_note(todo.body, project: todo.project) + + - if todo.pending? + .todo-actions + = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do + Done + = icon('spinner spin') diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index ff379bafb26..0237e152b54 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -24,7 +24,7 @@ .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' - .file-content.code + .file-editor.code %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} - if local_assigns[:path] .js-edit-mode-pane#preview.hide diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index 18caddabd39..4c356d1f07f 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -1,9 +1,15 @@ .file-content.image_file - if blob.svg? - - # We need to scrub SVG but we cannot do so in the RawController: it would - - # be wrong/strange if RawController modified the data. - - blob.load_all_data!(@repository) - - blob = sanitize_svg(blob) - %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} + - if blob.size_within_svg_limits? + - # We need to scrub SVG but we cannot do so in the RawController: it would + - # be wrong/strange if RawController modified the data. + - blob.load_all_data!(@repository) + - blob = sanitize_svg(blob) + %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} + - else + .nothing-here-block + The SVG could not be displayed as it is too large, you can + #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')} + instead. - else %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))} diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index b1c9895f43e..7b0621f9401 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,5 +1,11 @@ - page_title "Edit", @blob.path, @ref +- if @conflict + .alert.alert-danger + Someone edited the file the same time you did. Please check out + = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank" + and make sure your changes will not unintentionally remove theirs. + .file-editor %ul.nav-links.no-bottom.js-edit-mode %li.active @@ -13,8 +19,7 @@ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" - - = hidden_field_tag 'last_commit', @last_commit + = hidden_field_tag 'last_commit_sha', @last_commit_sha = hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id) diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml index 8151187d499..3fcf1692e09 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/hooks/_project_hook.html.haml @@ -3,7 +3,7 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray.deploy-project-label= trigger.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index e5cce16a171..9f1a046ea74 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -37,14 +37,19 @@ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) + - if @issue.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' + - if can?(current_user, :create_issue, @project) = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do - Edit + - if @issue.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' + = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' .issue-details.issuable-details diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 6ef640bb654..494695a03a5 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -42,3 +42,16 @@ .ci_widget.ci-error{style: "display:none"} = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. + +- @merge_request.environments.each do |environment| + .mr-widget-heading + .ci_widget.ci-success + = ci_icon_for_status("success") + %span.hidden-sm + Deployed to + = succeed '.' do + = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment' + - external_url = environment.external_url + - if external_url + = link_to external_url, target: '_blank' do + = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index adcc984f506..ea4898f2107 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -77,7 +77,7 @@ = link_to "#", class: 'btn js-toggle-button import_git' do = icon('git', text: 'Repo by URL') %div{ class: 'import_gitlab_project' } - - if gitlab_project_import_enabled? + - if gitlab_project_import_enabled? && current_user.is_admin? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = icon('gitlab', text: 'GitLab export') diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml new file mode 100644 index 00000000000..7b7fa56d993 --- /dev/null +++ b/app/views/projects/pipelines_settings/_badge.html.haml @@ -0,0 +1,27 @@ +.row{ class: badge.title.gsub(' ', '-') } + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + = badge.title.capitalize + .col-lg-9 + .prepend-top-10 + .panel.panel-default + .panel-heading + %b + = badge.title.capitalize + · + = badge.to_html + .pull-right + = render 'shared/ref_switcher', destination: 'badges', align_right: true + .panel-body + .row + .col-md-2.text-center + Markdown + .col-md-10.code.js-syntax-highlight + = highlight('.md', badge.to_markdown) + .row + %hr + .row + .col-md-2.text-center + HTML + .col-md-10.code.js-syntax-highlight + = highlight('.html', badge.to_html) diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 228bad36ebd..8c7222bfe3d 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -77,27 +77,4 @@ %hr .row.prepend-top-default - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - Builds Badge - .col-lg-9 - .prepend-top-10 - .panel.panel-default - .panel-heading - %b Builds badge · - = @build_badge.to_html - .pull-right - = render 'shared/ref_switcher', destination: 'badges', align_right: true - .panel-body - .row - .col-md-2.text-center - Markdown - .col-md-10.code.js-syntax-highlight - = highlight('.md', @build_badge.to_markdown) - .row - %hr - .row - .col-md-2.text-center - HTML - .col-md-10.code.js-syntax-highlight - = highlight('.html', @build_badge.to_html) + = render partial: 'badge', collection: @badges diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index 85d0c494ba8..d4c6fa24768 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -5,6 +5,7 @@ Protect a branch .panel-body .form-horizontal + = form_errors(@protected_branch) .form-group = f.label :name, class: 'col-md-2 text-right' do Branch: @@ -18,19 +19,19 @@ %code production/* are supported .form-group - %label.col-md-2.text-right{ for: 'merge_access_level_attributes' } + %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } Allowed to merge: .col-md-10 = dropdown_tag('Select', options: { toggle_class: 'js-allowed-to-merge wide', - data: { field_name: 'protected_branch[merge_access_level_attributes][access_level]', input_id: 'merge_access_level_attributes' }}) + data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }}) .form-group - %label.col-md-2.text-right{ for: 'push_access_level_attributes' } + %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } Allowed to push: .col-md-10 = dropdown_tag('Select', options: { toggle_class: 'js-allowed-to-push wide', - data: { field_name: 'protected_branch[push_access_level_attributes][access_level]', input_id: 'push_access_level_attributes' }}) + data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }}) .panel-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index e2e01ee78f8..0628134b1bb 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -13,16 +13,9 @@ = time_ago_with_tooltip(commit.committed_date) - else (branch was removed from repository) - %td - = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level - = dropdown_tag( (protected_branch.merge_access_level.humanize || 'Select') , - options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container', - data: { field_name: "allowed_to_merge_#{protected_branch.id}" }}) - %td - = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level - = dropdown_tag( (protected_branch.push_access_level.humanize || 'Select') , - options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', - data: { field_name: "allowed_to_push_#{protected_branch.id}" }}) + + = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch } + - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml new file mode 100644 index 00000000000..d6044aacaec --- /dev/null +++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml @@ -0,0 +1,10 @@ +%td + = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level + = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container', + data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }}) +%td + = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level + = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', + data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }}) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0b7fa8c7d06..c957cd84479 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -45,7 +45,7 @@ .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } .filter-item.inline = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index c30bdb0ae91..210b43c7e0b 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -2,7 +2,22 @@ .form-group = f.label :title, class: 'control-label' - .col-sm-10 + + - issuable_template_names = issuable_templates(issuable) + + - if issuable_template_names.any? + .col-sm-3.col-lg-2 + .js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } } + - title = selected_template(issuable) || "Choose a template" + + = dropdown_tag(title, options: { toggle_class: 'js-issuable-selector', + title: title, filter: true, placeholder: 'Filter', footer_content: true, + data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do + %ul.dropdown-footer-list + %li + %a.reset-template + Reset template + %div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' } = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', class: 'form-control pad', required: true @@ -23,6 +38,13 @@ to prevent a %strong Work In Progress merge request from being merged before it's ready. + + - if can_add_template?(issuable) + %p.help-block + Add + = link_to "issuable templates", help_page_path('workflow/description_templates') + to help your contributors communicate effectively! + .form-group.detail-page-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 92803838d02..281ec728e41 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -12,6 +12,8 @@ %li.project-row{ class: css_class } = cache(cache_key) do .controls + - if project.archived + %span.label.label-warning archived - if project.commit.try(:status) %span = render_commit_status(project.commit) diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 470dac6d75b..d2ec6c3ddef 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -29,49 +29,56 @@ = f.label :push_events, class: 'list-label' do %strong Push events %p.light - This url will be triggered by a push to the repository + This URL will be triggered by a push to the repository %li = f.check_box :tag_push_events, class: 'pull-left' .prepend-left-20 = f.label :tag_push_events, class: 'list-label' do %strong Tag push events %p.light - This url will be triggered when a new tag is pushed to the repository + This URL will be triggered when a new tag is pushed to the repository %li = f.check_box :note_events, class: 'pull-left' .prepend-left-20 = f.label :note_events, class: 'list-label' do %strong Comments %p.light - This url will be triggered when someone adds a comment + This URL will be triggered when someone adds a comment %li = f.check_box :issues_events, class: 'pull-left' .prepend-left-20 = f.label :issues_events, class: 'list-label' do %strong Issues events %p.light - This url will be triggered when an issue is created/updated/merged + This URL will be triggered when an issue is created/updated/merged %li = f.check_box :merge_requests_events, class: 'pull-left' .prepend-left-20 = f.label :merge_requests_events, class: 'list-label' do %strong Merge Request events %p.light - This url will be triggered when a merge request is created/updated/merged + This URL will be triggered when a merge request is created/updated/merged %li = f.check_box :build_events, class: 'pull-left' .prepend-left-20 = f.label :build_events, class: 'list-label' do %strong Build events %p.light - This url will be triggered when the build status changes + This URL will be triggered when the build status changes + %li + = f.check_box :pipeline_events, class: 'pull-left' + .prepend-left-20 + = f.label :pipeline_events, class: 'list-label' do + %strong Pipeline events + %p.light + This URL will be triggered when the pipeline status changes %li = f.check_box :wiki_page_events, class: 'pull-left' .prepend-left-20 = f.label :wiki_page_events, class: 'list-label' do %strong Wiki Page events %p.light - This url will be triggered when a wiki page is created/updated + This URL will be triggered when a wiki page is created/updated .form-group = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' .checkbox |