diff options
66 files changed, 928 insertions, 310 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 319c726e3a9..ed739972524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,13 @@ Please view this file on the master branch, on stable branches it's out of date. - Trim leading and trailing whitespace on project_path (Linus Thiel) - Prevent award emoji via notes for issues/MRs authored by user (barthc) - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) +- Fix Markdown styling inside reference links (Jan Zdráhal) - Fix extra space on Build sidebar on Firefox !7060 +- Fail gracefully when creating merge request with non-existing branch (alexsanford) - Fix mobile layout issues in admin user overview page !7087 - Fix HipChat notifications rendering (airatshigapov, eisnerd) - Refactor Jira service to use jira-ruby gem +- Improved todos empty state - Add hover to trash icon in notes !7008 (blackst0ne) - Hides project activity tabs when features are disabled - Only show one error message for an invalid email !5905 (lycoperdon) @@ -20,6 +23,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Removed delete branch tooltip !6954 - Stop unauthorized users dragging on milestone page (blackst0ne) - Restore issue boards welcome message when a project is created !6899 +- Do not show tooltip for active element !7105 (winniehell) - Escape ref and path for relative links !6050 (winniehell) - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) - Fix filtering of milestones with quotes in title (airatshigapov) @@ -38,9 +42,18 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens - Show full status link on MR & commit pipelines - Fix documents and comments on Build API `scope` +- Initialize Sidekiq with the list of queues used by GitLab - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) - Shortened merge request modal to let clipboard button not overlap - In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo) +- Improve search query parameter naming in /admin/users !7115 (YarNayar) +- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar) + +## 8.13.3 + +- Fix relative links in Markdown wiki when displayed in "Project" tab !7218 +- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project +- Fix project features default values ## 8.13.2 (2016-10-31) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e57cf1b3a58..7dd9adac736 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,6 +1,6 @@ /* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. @@ -188,6 +188,7 @@ // Close select2 on escape }); // Initialize tooltips + $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', placement: function(_, el) { diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 056baf66525..e103748d499 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,6 +1,6 @@ /* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index ede72a96d76..42d6799c82f 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,6 +1,6 @@ /* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index a18d16ea46c..cfd1e2204d5 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -11,11 +11,9 @@ $this.parent().find('.star-count').text(data.star_count); if (isStarred) { $starSpan.removeClass('starred').text('Star'); - gl.utils.updateTooltipTitle($this, 'Star project'); $starIcon.removeClass('fa-star').addClass('fa-star-o'); } else { $starSpan.addClass('starred').text('Unstar'); - gl.utils.updateTooltipTitle($this, 'Unstar project'); $starIcon.removeClass('fa-star-o').addClass('fa-star'); } }; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index c0e9c8bf829..ed21ad83a1c 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -216,7 +216,7 @@ svg, .fa { &:not(:last-child) { - margin-right: 3px; + margin-right: 5px; } } } diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 9282e0ae03b..486ad16ea26 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,7 +1,7 @@ .awards { .emoji-icon { - width: 20px; - height: 20px; + width: 19px; + height: 19px; } } @@ -94,7 +94,7 @@ .award-control { margin: 3px 5px 3px 0; - padding: 6px 5px; + padding: 5px 6px; outline: 0; &:hover, @@ -127,7 +127,7 @@ .award-control-icon { float: left; margin-right: 5px; - font-size: 20px; + font-size: 19px; } .award-control-icon-loading { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index b90c91831f2..526e9ae5cdd 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -105,11 +105,6 @@ ul.notes { padding: 2px; margin-top: 10px; } - - .award-control { - font-size: 13px; - padding: 2px 5px; - } } .note-header { diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index ea76fe18876..b3aef2fdd32 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -161,3 +161,63 @@ } } } + +.todos-empty { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + max-width: 900px; + margin-left: auto; + margin-right: auto; + + @media (min-width: $screen-sm-min) { + -webkit-flex-direction: row; + flex-direction: row; + padding-top: 80px; + } +} + +.todos-empty-content { + -webkit-align-self: center; + align-self: center; + max-width: 480px; + margin-right: 20px; +} + +.todos-empty-hero { + width: 200px; + margin-left: auto; + margin-right: auto; + + @media (min-width: $screen-sm-min) { + width: 300px; + margin-right: 0; + -webkit-order: 2; + order: 2; + } +} + +.todos-all-done { + padding-top: 20px; + + @media (min-width: $screen-sm-min) { + padding-top: 50px; + } + + > svg { + display: block; + max-width: 300px; + margin: 0 auto 20px; + } + + p { + max-width: 470px; + margin-left: auto; + margin-right: auto; + } + + a { + font-weight: 600; + } +} diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f35f4a8c811..bb912ed10cc 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -3,7 +3,7 @@ class Admin::UsersController < Admin::ApplicationController def index @users = User.order_name_asc.filter(params[:filter]) - @users = @users.search(params[:name]) if params[:name].present? + @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present? @users = @users.sort(@sort = params[:sort]) @users = @users.page(params[:page]) end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 4f855134368..42fd09e9b7e 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -126,7 +126,7 @@ class Projects::LabelsController < Projects::ApplicationController alias_method :subscribable_resource, :label def find_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities) + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end def authorize_admin_labels! diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index d08f490de18..699a56ae2f8 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -25,18 +25,15 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def create - if params[:user_ids].blank? - return redirect_to(namespace_project_project_members_path(@project.namespace, @project), alert: 'No users or groups specified.') - end + status = Members::CreateService.new(@project, current_user, params).execute - @project.team.add_users( - params[:user_ids].split(','), - params[:access_level], - expires_at: params[:expires_at], - current_user: current_user - ) + redirect_url = namespace_project_project_members_path(@project.namespace, @project) - redirect_to namespace_project_project_members_path(@project.namespace, @project), notice: 'Users were successfully added.' + if status + redirect_to redirect_url, notice: 'Users were successfully added.' + else + redirect_to redirect_url, alert: 'No users or groups specified.' + end end def update diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8c148ecfaeb..bce5e29d8d8 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -289,7 +289,8 @@ class ProjectsController < Projects::ApplicationController render 'projects/empty' if @project.empty_repo? else if @project.wiki_enabled? - @wiki_home = @project.wiki.find_page('home', params[:version_id]) + @project_wiki = @project.wiki + @wiki_home = @project_wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) @issues = issues_collection @issues = @issues.page(params[:page]) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index e27986ef95b..cc2073081b5 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -126,7 +126,7 @@ class IssuableFinder @labels = if labels? && !filter_by_no_label? - LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute + LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true) else Label.none end @@ -273,7 +273,7 @@ class IssuableFinder items = items.with_label(label_names, params[:sort]) if projects - label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id) + label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id) items = items.where(labels: { id: label_ids }) end end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 9216122923e..6d88951c713 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -31,7 +31,7 @@ module ProjectFeaturesCompatibility def write_feature_attribute(field, value) build_project_feature unless project_feature - access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED project_feature.update_attribute(field, access_level) end end diff --git a/app/models/group_label.rb b/app/models/group_label.rb index a698b532d19..68841ace2e6 100644 --- a/app/models/group_label.rb +++ b/app/models/group_label.rb @@ -5,6 +5,10 @@ class GroupLabel < Label alias_attribute :subject, :group + def subject_foreign_key + 'group_id' + end + def to_reference(source_project = nil, target_project = nil, format: :id) super(source_project, target_project, format: format) end diff --git a/app/models/label.rb b/app/models/label.rb index 149fd98ecb3..d9287f2dc29 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -92,16 +92,23 @@ class Label < ActiveRecord::Base nil end - def open_issues_count(user = nil, project = nil) - issues_count(user, project_id: project.try(:id) || project_id, state: 'opened') + def open_issues_count(user = nil) + issues_count(user, state: 'opened') end - def closed_issues_count(user = nil, project = nil) - issues_count(user, project_id: project.try(:id) || project_id, state: 'closed') + def closed_issues_count(user = nil) + issues_count(user, state: 'closed') end - def open_merge_requests_count(user = nil, project = nil) - merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened') + def open_merge_requests_count(user = nil) + params = { + subject_foreign_key => subject.id, + label_name: title, + scope: 'all', + state: 'opened' + } + + MergeRequestsFinder.new(user, params.with_indifferent_access).execute.count end def prioritize!(project, value) @@ -167,15 +174,8 @@ class Label < ActiveRecord::Base end def issues_count(user, params = {}) - IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) - .execute - .count - end - - def merge_requests_count(user, params = {}) - MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all')) - .execute - .count + params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all') + IssuesFinder.new(user, params.with_indifferent_access).execute.count end def label_format_reference(format = :id) diff --git a/app/models/project.rb b/app/models/project.rb index ae57815c620..d5512dfaf9c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -30,6 +30,11 @@ class Project < ActiveRecord::Base default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { current_application_settings.repository_storage } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } + default_value_for :issues_enabled, gitlab_config_features.issues + default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests + default_value_for :builds_enabled, gitlab_config_features.builds + default_value_for :wiki_enabled, gitlab_config_features.wiki + default_value_for :snippets_enabled, gitlab_config_features.snippets after_create :ensure_dir_exist after_create :create_project_feature, unless: :project_feature @@ -390,7 +395,7 @@ class Project < ActiveRecord::Base end def group_ids - joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id) + joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end end diff --git a/app/models/project_label.rb b/app/models/project_label.rb index 33c2b617715..82f47f0e8fd 100644 --- a/app/models/project_label.rb +++ b/app/models/project_label.rb @@ -12,6 +12,10 @@ class ProjectLabel < Label alias_attribute :subject, :project + def subject_foreign_key + 'project_id' + end + def to_reference(target_project = nil, format: :id) super(project, target_project, format: format) end diff --git a/app/models/user.rb b/app/models/user.rb index e2a97c3a757..af3c0b7dc02 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -258,6 +258,24 @@ class User < ActiveRecord::Base ) end + # searches user by given pattern + # it compares name, email, username fields and user's secondary emails with given pattern + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + + def search_with_secondary_emails(query) + table = arel_table + email_table = Email.arel_table + pattern = "%#{query}%" + matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern)) + + where( + table[:name].matches(pattern). + or(table[:email].matches(pattern)). + or(table[:username].matches(pattern)). + or(table[:id].in(matched_by_emails_user_ids)) + ) + end + def by_login(login) return nil unless login diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb new file mode 100644 index 00000000000..e4b24ccef92 --- /dev/null +++ b/app/services/members/create_service.rb @@ -0,0 +1,16 @@ +module Members + class CreateService < BaseService + def execute + return false if params[:user_ids].blank? + + project.team.add_users( + params[:user_ids].split(','), + params[:access_level], + expires_at: params[:expires_at], + current_user: current_user + ) + + true + end + end +end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 404f75616b5..f415244068b 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -13,20 +13,8 @@ module MergeRequests merge_request.target_project ||= (project.forked_from_project || project) merge_request.target_branch ||= merge_request.target_project.default_branch - if merge_request.target_branch.blank? || merge_request.source_branch.blank? - message = - if params[:source_branch] || params[:target_branch] - "You must select source and target branch" - end - - return build_failed(merge_request, message) - end - - if merge_request.source_project == merge_request.target_project && - merge_request.target_branch == merge_request.source_branch - - return build_failed(merge_request, 'You must select different branches') - end + messages = validate_branches(merge_request) + return build_failed(merge_request, messages) unless messages.empty? compare = CompareService.new.execute( merge_request.source_project, @@ -43,6 +31,34 @@ module MergeRequests private + def validate_branches(merge_request) + messages = [] + + if merge_request.target_branch.blank? || merge_request.source_branch.blank? + messages << + if params[:source_branch] || params[:target_branch] + "You must select source and target branch" + end + end + + if merge_request.source_project == merge_request.target_project && + merge_request.target_branch == merge_request.source_branch + + messages << 'You must select different branches' + end + + # See if source and target branches exist + unless merge_request.source_project.commit(merge_request.source_branch) + messages << "Source branch \"#{merge_request.source_branch}\" does not exist" + end + + unless merge_request.target_project.commit(merge_request.target_branch) + messages << "Target branch \"#{merge_request.target_branch}\" does not exist" + end + + messages + end + # When your branch name starts with an iid followed by a dash this pattern will be # interpreted as the user wants to close that issue on this project. # @@ -91,8 +107,10 @@ module MergeRequests merge_request end - def build_failed(merge_request, message) - merge_request.errors.add(:base, message) unless message.nil? + def build_failed(merge_request, messages) + messages.compact.each do |message| + merge_request.errors.add(:base, message) + end merge_request.compare_commits = [] merge_request.can_be_created = false merge_request diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 357123c2c13..d3038ae644f 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -10,7 +10,7 @@ = hidden_field_tag "filter", h(params[:filter]) .search-holder .search-field-holder - = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false + = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false = icon("search", class: "search-icon") .dropdown - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 2a0302638ba..2411cc45724 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -1,69 +1,70 @@ - page_title "Todos" - header_title "Todos", dashboard_todos_path -.top-area - %ul.nav-links - - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') - %li{class: "todos-pending #{todo_pending_active}"} - = link_to todos_filter_path(state: 'pending') do - %span - To do - %span.badge - = number_with_delimiter(todos_pending_count) - - todo_done_active = ('active' if params[:state] == 'done') - %li{class: "todos-done #{todo_done_active}"} - = link_to todos_filter_path(state: 'done') do - %span - Done - %span.badge - = number_with_delimiter(todos_done_count) +- if current_user.todos.any? + .top-area + %ul.nav-links + - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') + %li{class: "todos-pending #{todo_pending_active}"} + = link_to todos_filter_path(state: 'pending') do + %span + To do + %span.badge + = number_with_delimiter(todos_pending_count) + - todo_done_active = ('active' if params[:state] == 'done') + %li{class: "todos-done #{todo_done_active}"} + = link_to todos_filter_path(state: 'done') do + %span + Done + %span.badge + = number_with_delimiter(todos_done_count) - .nav-controls - - if @todos.any?(&:pending?) - = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do - Mark all as done - = icon('spinner spin') + .nav-controls + - if @todos.any?(&:pending?) + = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do + Mark all as done + = icon('spinner spin') -.todos-filters - .row-content-block.second-block - = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do - .filter-item.inline - - if params[:project_id].present? - = hidden_field_tag(:project_id, params[:project_id]) - = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options } }) - .filter-item.inline - - if params[:author_id].present? - = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', - placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } }) - .filter-item.inline - - if params[:type].present? - = hidden_field_tag(:type, params[:type]) - = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options } }) - .filter-item.inline.actions-filter - - if params[:action_id].present? - = hidden_field_tag(:action_id, params[:action_id]) - = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options }}) - .pull-right - .dropdown.inline.prepend-left-10 - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort - %li - = link_to todos_filter_path(sort: sort_value_priority) do - = sort_title_priority - = link_to todos_filter_path(sort: sort_value_recently_created) do + .todos-filters + .row-content-block.second-block + = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do + .filter-item.inline + - if params[:project_id].present? + = hidden_field_tag(:project_id, params[:project_id]) + = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', + placeholder: 'Search projects', data: { data: todo_projects_options } }) + .filter-item.inline + - if params[:author_id].present? + = hidden_field_tag(:author_id, params[:author_id]) + = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', + placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } }) + .filter-item.inline + - if params[:type].present? + = hidden_field_tag(:type, params[:type]) + = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', + data: { data: todo_types_options } }) + .filter-item.inline.actions-filter + - if params[:action_id].present? + = hidden_field_tag(:action_id, params[:action_id]) + = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', + data: { data: todo_actions_options }}) + .pull-right + .dropdown.inline.prepend-left-10 + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else = sort_title_recently_created - = link_to todos_filter_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %li + = link_to todos_filter_path(sort: sort_value_priority) do + = sort_title_priority + = link_to todos_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to todos_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created .prepend-top-default @@ -78,5 +79,29 @@ %ul.content-list.todos-list = render group[1] = paginate @todos, theme: "gitlab" + - elsif current_user.todos.any? + .todos-all-done + = render "shared/empty_states/todos_all_done.svg" + %h4.text-center + Good job! Looks like you don't have any todos left. + %p.text-center + Are you looking for things to do? Take a look at + = succeed "," do + = link_to "the opened issues", issues_dashboard_path + contribute to + = link_to "merge requests", merge_requests_dashboard_path + or mention someone in a comment to assign a new todo automatically. - else - .nothing-here-block You're all done! + .todos-empty + .todos-empty-hero + = render "shared/empty_states/todos_empty.svg" + .todos-empty-content + %h4 + Todos let you see what you should do next. + %p + When an issue or merge request is assigned to you, or when you + %strong + @mention + in a comment, this will trigger a new item in your todo list, automatically. + %p + You will always know what to work on next. diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 70783a63409..45325d6bc4b 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -13,7 +13,7 @@ .other-labels - if @labels.present? %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', collection: @labels, as: :label + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - else .nothing-here-block diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 29d549a60f5..27da86b9efe 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -5,10 +5,10 @@ = custom_icon('icon_fork') %span Fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do = custom_icon('icon_fork') %span Fork %div.count-with-arrow %span.arrow - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 311583037e5..12d35101770 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,5 +1,5 @@ - if current_user - = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do + = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do - if current_user.starred?(@project) = icon('star') %span.starred Unstar diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index f135bf6f6b4..05a8475dcd6 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -22,14 +22,14 @@ %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet - if @prioritized_labels.present? - = render partial: 'shared/label', collection: @prioritized_labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label .other-labels - if can?(current_user, :admin_label, @project) %h5{ class: ('hide' if hide) } Other Labels %ul.content-list.manage-labels-list.js-other-labels - if @labels.present? - = render partial: 'shared/label', collection: @labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - if @labels.blank? .nothing-here-block diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 40c8d2af226..6ccdef0df46 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,6 +1,7 @@ - label_css_id = dom_id(label) -- open_issues_count = label.open_issues_count(current_user, @project) -- open_merge_requests_count = label.open_merge_requests_count(current_user, @project) +- open_issues_count = label.open_issues_count(current_user) +- open_merge_requests_count = label.open_merge_requests_count(current_user) +- subject = local_assigns[:subject] %li{id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label @@ -12,10 +13,10 @@ .dropdown-menu.dropdown-menu-align-right %ul %li - = link_to_label(label, subject: @project, type: :merge_request) do + = link_to_label(label, subject: subject, type: :merge_request) do = pluralize open_merge_requests_count, 'merge request' %li - = link_to_label(label, subject: @project) do + = link_to_label(label, subject: subject) do = pluralize open_issues_count, 'open issue' - if current_user %li.label-subscription{ data: toggle_subscription_data(label) } @@ -28,9 +29,9 @@ = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'} .pull-right.hidden-xs.hidden-sm.hidden-md - = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do + = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do = pluralize open_merge_requests_count, 'merge request' - = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do + = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do = pluralize open_issues_count, 'open issue' - if current_user diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/_todos_all_done.svg new file mode 100644 index 00000000000..94b5c2e0ea0 --- /dev/null +++ b/app/views/shared/empty_states/_todos_all_done.svg @@ -0,0 +1 @@ +<svg viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg> diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/_todos_empty.svg new file mode 100644 index 00000000000..b1e661268fb --- /dev/null +++ b/app/views/shared/empty_states/_todos_empty.svg @@ -0,0 +1,110 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/> + <mask id="l" width="180" height="220" x="0" y="0" fill="#fff"> + <use xlink:href="#a"/> + </mask> + <rect id="b" width="180" height="220" rx="6"/> + <mask id="m" width="180" height="220" x="0" y="0" fill="#fff"> + <use xlink:href="#b"/> + </mask> + <rect id="c" width="28" height="28" rx="4"/> + <mask id="n" width="28" height="28" x="0" y="0" fill="#fff"> + <use xlink:href="#c"/> + </mask> + <rect id="d" width="28" height="28" rx="4"/> + <mask id="o" width="28" height="28" x="0" y="0" fill="#fff"> + <use xlink:href="#d"/> + </mask> + <circle id="e" cx="21.5" cy="21.5" r="21.5"/> + <mask id="p" width="43" height="43" x="0" y="0" fill="#fff"> + <use xlink:href="#e"/> + </mask> + <circle id="f" cx="26.5" cy="26.5" r="26.5"/> + <mask id="q" width="53" height="53" x="0" y="0" fill="#fff"> + <use xlink:href="#f"/> + </mask> + <circle id="g" cx="9.5" cy="4.5" r="4.5"/> + <mask id="r" width="13" height="13" x="-2" y="-2"> + <path fill="#fff" d="M3-2h13v13H3z"/> + <use xlink:href="#g"/> + </mask> + <circle id="h" cx="26.5" cy="26.5" r="26.5"/> + <mask id="s" width="53" height="53" x="0" y="0" fill="#fff"> + <use xlink:href="#h"/> + </mask> + <circle id="i" cx="21.5" cy="21.5" r="21.5"/> + <mask id="t" width="43" height="43" x="0" y="0" fill="#fff"> + <use xlink:href="#i"/> + </mask> + <path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/> + <mask id="u" width="52" height="44" x="0" y="0" fill="#fff"> + <use xlink:href="#j"/> + </mask> + <circle id="k" cx="18.5" cy="18.5" r="18.5"/> + <mask id="v" width="37" height="37" x="0" y="0" fill="#fff"> + <use xlink:href="#k"/> + </mask> + </defs> + <g fill="none" fill-rule="evenodd" transform="translate(-6 -4)"> + <use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/> + <g transform="rotate(5 -707.333 618.042)"> + <use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/> + <g transform="translate(29 24)"> + <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/> + <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/> + <rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/> + <rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/> + </g> + <g transform="translate(29 69)"> + <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/> + <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/> + <rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/> + <rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/> + </g> + <g transform="translate(28 160)"> + <use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/> + <rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/> + <rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/> + </g> + <g transform="translate(28 116)"> + <use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/> + <rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/> + <rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/> + </g> + </g> + <g transform="rotate(-15 601.917 -782.362)"> + <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/> + <text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1"> + <tspan x="12" y="27">@</tspan> + </text> + </g> + <g transform="rotate(15 -686.59 1035.907)"> + <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/> + <path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/> + <g transform="translate(17 14)"> + <use fill="#FC6D26" xlink:href="#g"/> + <use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/> + </g> + </g> + <g transform="rotate(15 -85.125 65.185)"> + <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/> + <path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/> + </g> + <g transform="rotate(-15 716.492 78.873)"> + <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/> + <path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/> + </g> + <g transform="rotate(-15 129.114 -585.74)"> + <use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/> + <circle cx="16" cy="20" r="2" fill="#FC6D26"/> + <circle cx="27" cy="20" r="2" fill="#FC6D26"/> + <circle cx="38" cy="20" r="2" fill="#FC6D26"/> + </g> + <g transform="rotate(-15 1254.8 -458.986)"> + <use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/> + <path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/> + <rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/> + </g> + </g> +</svg> diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 74325872b09..c11296975b7 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -101,6 +101,13 @@ :why: GPL-licensed libraries cannot be linked to from non-GPL projects. :versions: [] :when: 2016-05-02 05:29:43.904715000 Z +- - :blacklist + - OSL-3.0 + - :who: Sean McGivern + :why: The OSL license is a copyleft license + :versions: [] + :when: 2016-10-28 11:02:15.540105000 Z + # GEM LICENSES - - :license diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index f7e714cd6bc..0455a98dbfe 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -42,3 +42,19 @@ end Sidekiq.configure_client do |config| config.redis = redis_config_hash end + +# The Sidekiq client API always adds the queue to the Sidekiq queue +# list, but mail_room and gitlab-shell do not. This is only necessary +# for monitoring. +config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s) + +begin + Sidekiq.redis do |conn| + conn.pipelined do + config[:queues].each do |queue| + conn.sadd('queues', queue[0]) + end + end + end +rescue Redis::BaseError, SocketError +end diff --git a/doc/api/README.md b/doc/api/README.md index 3fbe5197a21..f65b934b9db 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -23,6 +23,7 @@ following locations: - [Group Access Requests](access_requests.md) - [Group Members](members.md) - [Issues](issues.md) +- [Issue Boards](boards.md) - [Keys](keys.md) - [Labels](labels.md) - [Merge Requests](merge_requests.md) diff --git a/doc/api/projects.md b/doc/api/projects.md index b69db90e70d..8ebac57e612 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -598,7 +598,7 @@ Parameters: | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | -| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] | +| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | @@ -630,7 +630,7 @@ Parameters: | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | -| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] | +| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | @@ -661,7 +661,7 @@ Parameters: | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | | `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 | -| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] | +| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) | | `import_url` | string | no | URL to import repository from | | `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members | | `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds | diff --git a/doc/api/tags.md b/doc/api/tags.md index 54059117456..398b080e3f6 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -124,7 +124,7 @@ Parameters: The message will be `nil` when creating a lightweight tag otherwise it will contain the annotation. -It returns 200 if the operation succeed. In case of an error, +It returns 201 if the operation succeed. In case of an error, 405 with an explaining error message is returned. ## Delete a tag diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md index 159d5ce286d..b25ce79e89f 100644 --- a/doc/development/gotchas.md +++ b/doc/development/gotchas.md @@ -41,9 +41,9 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9 [Exception]: http://stackoverflow.com/q/10048173/223897 -## Don't use inline CoffeeScript/JavaScript in views +## Don't use inline JavaScript in views -Using the inline `:coffee` or `:coffeescript` Haml filters comes with a +Using the inline `:javascript` Haml filters comes with a performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided. _**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb) @@ -51,9 +51,7 @@ in an initializer._ ### Further reading -- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu) - Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting) -- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897) ## ID-based CSS selectors need to be a bit more specific diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 05972b33fdb..5d177eb26ee 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -62,6 +62,7 @@ Libraries with the following licenses are unacceptable for use: - [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. - [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. +- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU]. ## Notes @@ -93,3 +94,5 @@ Gems which are included only in the "development" or "test" groups by Bundler ar [AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/ [GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL [OSI-GPL]: https://opensource.org/faq#linking-proprietary-code +[OSL]: https://opensource.org/licenses/OSL-3.0 +[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL diff --git a/doc/development/testing.md b/doc/development/testing.md index 513457d203a..8e91ac5e3ba 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -36,8 +36,8 @@ the command line via `bundle exec teaspoon`, or via a web browser at `http://localhost:3000/teaspoon` when the Rails server is running. - JavaScript tests live in `spec/javascripts/`, matching the folder structure of - `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding - `spec/javascripts/behaviors/autosize_spec.js.coffee` file. + `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding + `spec/javascripts/behaviors/autosize_spec.js.es6` file. - Haml fixtures required for JavaScript tests live in `spec/javascripts/fixtures`. They should contain the bare minimum amount of markup necessary for the test. diff --git a/doc/integration/saml.md b/doc/integration/saml.md index f3b2a288776..4a242c321aa 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -268,13 +268,20 @@ message `Can't verify CSRF token authenticity`. This means that there is an erro the SAML request, but this error never reaches GitLab due to the CSRF check. To bypass this you can add `skip_before_action :verify_authenticity_token` to the -`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, -where it can then be seen in the usual logs, or as a flash message in the login -screen. - -That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers` -for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for -installations from source. +`omniauth_callbacks_controller.rb` file immediately after the `class` line and +comment out the `protect_from_forgery` line using a `#` then restart Unicorn. This +will allow the error to hit GitLab, where it can then be seen in the usual logs, +or as a flash message on the login screen. + +That file is located in `/opt/gitlab/embedded/service/gitlab-rails/app/controllers` +for Omnibus installations and by default in `/home/git/gitlab/app/controllers` for +installations from source. Restart Unicorn using the `sudo gitlab-ctl restart unicorn` +command on Omnibus installations and `sudo service gitlab restart` on installations +from source. + +You may also find the [SSO Tracer](https://addons.mozilla.org/en-US/firefox/addon/sso-tracer) +(Firefox) and [SAML Chrome Panel](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace) +(Chrome) browser extensions useful in your debugging. ### Invalid audience diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8025581d3ca..3c9d7b1aaef 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,18 +1,12 @@ module API module Helpers + include Gitlab::Utils + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo - def to_boolean(value) - return value if [true, false].include?(value) - return true if value =~ /^(true|t|yes|y|1|on)$/i - return false if value =~ /^(false|f|no|n|0|off)$/i - - nil - end - def private_token params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index cb213a76a05..3740d4fb4cd 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -102,10 +102,10 @@ module Banzai end elsif element_node?(node) - yield_valid_link(node) do |link, text| + yield_valid_link(node) do |link, inner_html| if ref_pattern && link =~ /\A#{ref_pattern}\z/ replace_link_node_with_href(node, link) do - object_link_filter(link, ref_pattern, link_text: text) + object_link_filter(link, ref_pattern, link_content: inner_html) end next @@ -113,9 +113,9 @@ module Banzai next unless link_pattern - if link == text && text =~ /\A#{link_pattern}/ + if link == inner_html && inner_html =~ /\A#{link_pattern}/ replace_link_node_with_text(node, link) do - object_link_filter(text, link_pattern) + object_link_filter(inner_html, link_pattern) end next @@ -123,7 +123,7 @@ module Banzai if link =~ /\A#{link_pattern}\z/ replace_link_node_with_href(node, link) do - object_link_filter(link, link_pattern, link_text: text) + object_link_filter(link, link_pattern, link_content: inner_html) end next @@ -140,11 +140,11 @@ module Banzai # # text - String text to replace references in. # pattern - Reference pattern to match against. - # link_text - Original content of the link being replaced. + # link_content - Original content of the link being replaced. # # Returns a String with references replaced with links. All links # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. - def object_link_filter(text, pattern, link_text: nil) + def object_link_filter(text, pattern, link_content: nil) references_in(text, pattern) do |match, id, project_ref, matches| project = project_from_ref_cached(project_ref) @@ -152,7 +152,7 @@ module Banzai title = object_link_title(object) klass = reference_class(object_sym) - data = data_attributes_for(link_text || match, project, object) + data = data_attributes_for(link_content || match, project, object) if matches.names.include?("url") && matches[:url] url = matches[:url] @@ -160,11 +160,11 @@ module Banzai url = url_for_object_cached(object, project) end - text = link_text || object_link_text(object, matches) + content = link_content || object_link_text(object, matches) %(<a href="#{url}" #{data} title="#{escape_once(title)}" - class="#{klass}">#{escape_once(text)}</a>) + class="#{klass}">#{content}</a>) else match end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index 0d20be557a0..dce4de3ceaf 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -37,10 +37,10 @@ module Banzai end elsif element_node?(node) - yield_valid_link(node) do |link, text| + yield_valid_link(node) do |link, inner_html| if link =~ ref_start_pattern replace_link_node_with_href(node, link) do - issue_link_filter(link, link_text: text) + issue_link_filter(link, link_content: inner_html) end end end @@ -54,10 +54,11 @@ module Banzai # issue's details page. # # text - String text to replace references in. + # link_content - Original content of the link being replaced. # # Returns a String with `JIRA-123` references replaced with links. All # links have `gfm` and `gfm-issue` class names attached for styling. - def issue_link_filter(text, link_text: nil) + def issue_link_filter(text, link_content: nil) project = context[:project] self.class.references_in(text, issue_reference_pattern) do |match, id| @@ -69,11 +70,11 @@ module Banzai klass = reference_class(:issue) data = data_attribute(project: project.id, external_issue: id) - text = link_text || match + content = link_content || match %(<a href="#{url}" #{data} title="#{escape_once(title)}" - class="#{klass}">#{escape_once(text)}</a>) + class="#{klass}">#{content}</a>) end end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 2d221290f7e..84bfeac8041 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -85,14 +85,14 @@ module Banzai @nodes ||= each_node.to_a end - # Yields the link's URL and text whenever the node is a valid <a> tag. + # Yields the link's URL and inner HTML whenever the node is a valid <a> tag. def yield_valid_link(node) link = CGI.unescape(node.attr('href').to_s) - text = node.text + inner_html = node.inner_html return unless link.force_encoding('UTF-8').valid_encoding? - yield link, text + yield link, inner_html end def replace_text_when_pattern_matches(node, pattern) diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index c6302b586d3..f842b1fb779 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -35,10 +35,10 @@ module Banzai user_link_filter(content) end elsif element_node?(node) - yield_valid_link(node) do |link, text| + yield_valid_link(node) do |link, inner_html| if link =~ ref_pattern_start replace_link_node_with_href(node, link) do - user_link_filter(link, link_text: text) + user_link_filter(link, link_content: inner_html) end end end @@ -52,15 +52,16 @@ module Banzai # user's profile page. # # text - String text to replace references in. + # link_content - Original content of the link being replaced. # # Returns a String with `@user` references replaced with links. All links # have `gfm` and `gfm-project_member` class names attached for styling. - def user_link_filter(text, link_text: nil) + def user_link_filter(text, link_content: nil) self.class.references_in(text) do |match, username| if username == 'all' - link_to_all(link_text: link_text) + link_to_all(link_content: link_content) elsif namespace = namespaces[username] - link_to_namespace(namespace, link_text: link_text) || match + link_to_namespace(namespace, link_content: link_content) || match else match end @@ -102,49 +103,49 @@ module Banzai reference_class(:project_member) end - def link_to_all(link_text: nil) + def link_to_all(link_content: nil) project = context[:project] author = context[:author] if author && !project.team.member?(author) - link_text + link_content else url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) data = data_attribute(project: project.id, author: author.try(:id)) - text = link_text || User.reference_prefix + 'all' + content = link_content || User.reference_prefix + 'all' - link_tag(url, data, text, 'All Project and Group Members') + link_tag(url, data, content, 'All Project and Group Members') end end - def link_to_namespace(namespace, link_text: nil) + def link_to_namespace(namespace, link_content: nil) if namespace.is_a?(Group) - link_to_group(namespace.path, namespace, link_text: link_text) + link_to_group(namespace.path, namespace, link_content: link_content) else - link_to_user(namespace.path, namespace, link_text: link_text) + link_to_user(namespace.path, namespace, link_content: link_content) end end - def link_to_group(group, namespace, link_text: nil) + def link_to_group(group, namespace, link_content: nil) url = urls.group_url(group, only_path: context[:only_path]) data = data_attribute(group: namespace.id) - text = link_text || Group.reference_prefix + group + content = link_content || Group.reference_prefix + group - link_tag(url, data, text, namespace.name) + link_tag(url, data, content, namespace.name) end - def link_to_user(user, namespace, link_text: nil) + def link_to_user(user, namespace, link_content: nil) url = urls.user_url(user, only_path: context[:only_path]) data = data_attribute(user: namespace.owner_id) - text = link_text || User.reference_prefix + user + content = link_content || User.reference_prefix + user - link_tag(url, data, text, namespace.owner_name) + link_tag(url, data, content, namespace.owner_name) end - def link_tag(url, data, text, title) - %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{escape_once(text)}</a>) + def link_tag(url, data, link_content, title) + %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) end end end diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb index 0df3a72d1c4..de3ebe72720 100644 --- a/lib/banzai/redactor.rb +++ b/lib/banzai/redactor.rb @@ -41,10 +41,10 @@ module Banzai next if visible.include?(node) doc_data[:visible_reference_count] -= 1 - # The reference should be replaced by the original text, - # which is not always the same as the rendered text. - text = node.attr('data-original') || node.text - node.replace(text) + # The reference should be replaced by the original link's content, + # which is not always the same as the rendered one. + content = node.attr('data-original') || node.inner_html + node.replace(content) end end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index e59ead5d76c..4c395b4266e 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -13,5 +13,13 @@ module Gitlab def force_utf8(str) str.force_encoding(Encoding::UTF_8) end + + def to_boolean(value) + return value if [true, false].include?(value) + return true if value =~ /^(true|t|yes|y|1|on)$/i + return false if value =~ /^(false|f|no|n|0|off)$/i + + nil + end end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index b4f066d8600..2a7523c6512 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -14,49 +14,49 @@ describe Projects::ProjectMembersController do end describe 'POST create' do - context 'when users are added' do - let(:project_user) { create(:user) } + let(:project_user) { create(:user) } - before { sign_in(user) } + before { sign_in(user) } - context 'when user does not have enough rights' do - before { project.team << [user, :developer] } + context 'when user does not have enough rights' do + before { project.team << [user, :developer] } - it 'returns 404' do - post :create, namespace_id: project.namespace, - project_id: project, - user_ids: project_user.id, - access_level: Gitlab::Access::GUEST + it 'returns 404' do + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: project_user.id, + access_level: Gitlab::Access::GUEST - expect(response).to have_http_status(404) - expect(project.users).not_to include project_user - end + expect(response).to have_http_status(404) + expect(project.users).not_to include project_user end + end - context 'when user has enough rights' do - before { project.team << [user, :master] } + context 'when user has enough rights' do + before { project.team << [user, :master] } - it 'adds user to members' do - post :create, namespace_id: project.namespace, - project_id: project, - user_ids: project_user.id, - access_level: Gitlab::Access::GUEST - - expect(response).to set_flash.to 'Users were successfully added.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) - expect(project.users).to include project_user - end + it 'adds user to members' do + expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(true) - it 'adds no user to members' do - post :create, namespace_id: project.namespace, - project_id: project, - user_ids: '', - access_level: Gitlab::Access::GUEST + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: project_user.id, + access_level: Gitlab::Access::GUEST - expect(response).to set_flash.to 'No users or groups specified.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) - expect(project.users).not_to include project_user - end + expect(response).to set_flash.to 'Users were successfully added.' + expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + end + + it 'adds no user to members' do + expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(false) + + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: '', + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'No users or groups specified.' + expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) end end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index b963d1305b5..c68e1ea4af9 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -59,4 +59,12 @@ feature 'Create New Merge Request', feature: true, js: true do expect(page).to have_css('a.btn.active', text: 'Side-by-side') end end + + it 'does not allow non-existing branches' do + visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) + + expect(page).to have_content('The form contains the following errors') + expect(page).to have_content('Source branch "non-exist-source" does not exist') + expect(page).to have_content('Target branch "non-exist-target" does not exist') + end end diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb new file mode 100644 index 00000000000..b4f5f6b3fc5 --- /dev/null +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'Projects > Wiki > User views wiki in project page', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'when repository is disabled for project' do + before do + project.project_feature.update!( + repository_access_level: ProjectFeature::DISABLED, + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED + ) + end + + context 'when wiki homepage contains a link' do + before do + WikiPages::CreateService.new( + project, + user, + title: 'home', + content: '[some link](other-page)' + ).execute + end + + it 'displays the correct URL for the link' do + visit namespace_project_path(project.namespace, project) + expect(page).to have_link( + 'some link', + href: namespace_project_wiki_path( + project.namespace, + project, + 'other-page' + ) + ) + end + end + end +end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 230543cd175..3ae83ac082d 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -13,7 +13,7 @@ describe 'Dashboard Todos', feature: true do visit dashboard_todos_path end it 'shows "All done" message' do - expect(page).to have_content "You're all done!" + expect(page).to have_content "Todos let you see what you should do next." end end @@ -44,7 +44,7 @@ describe 'Dashboard Todos', feature: true do end it 'shows "All done" message' do - expect(page).to have_content("You're all done!") + expect(page).to have_content("Good job! Looks like you don't have any todos left.") end end @@ -64,7 +64,7 @@ describe 'Dashboard Todos', feature: true do end it 'shows "All done" message' do - expect(page).to have_content("You're all done!") + expect(page).to have_content("Good job! Looks like you don't have any todos left.") end end end @@ -152,7 +152,7 @@ describe 'Dashboard Todos', feature: true do within('.todos-pending-count') { expect(page).to have_content '0' } expect(page).to have_content 'To do 0' expect(page).to have_content 'Done 0' - expect(page).to have_content "You're all done!" + expect(page).to have_content "Good job! Looks like you don't have any todos left." end end end diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index bdce2465fbf..9cb8243ee2c 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -28,7 +28,7 @@ // setTimeout(Teaspoon.execute, 1000) // Matching files // By default Teaspoon will look for files that match -// _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path +// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path // and it'll be included in the default suite automatically. If you want to // customize suites, check out the configuration in teaspoon_env.rb // Manifest diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index 2f9343fadaf..fbf7a461fa5 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -8,6 +8,8 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do end shared_examples_for "external issue tracker" do + it_behaves_like 'a reference containing an element node' + it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index a2025672ad9..8f0b2db3e8e 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -22,6 +22,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'internal reference' do + it_behaves_like 'a reference containing an element node' + let(:reference) { issue.to_reference } it 'ignores valid references when using non-default tracker' do @@ -83,6 +85,20 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(link.attr('data-issue')).to eq issue.id.to_s end + it 'includes a data-original attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-original') + expect(link.attr('data-original')).to eq reference + end + + it 'does not escape the data-original attribute' do + inner_html = 'element <code>node</code> inside' + doc = reference_filter(%{<a href="#{reference}">#{inner_html}</a>}) + expect(doc.children.first.attr('data-original')).to eq inner_html + end + it 'supports an :only_path context' do doc = reference_filter("Issue #{reference}", only_path: true) link = doc.css('a').first.attr('href') @@ -101,6 +117,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'cross-project reference' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } @@ -141,6 +159,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'cross-project URL reference' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } @@ -160,39 +180,45 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do end context 'cross-project reference in link href' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} } + let(:reference) { issue.to_reference(project) } + let(:reference_link) { %{<a href="#{reference}">Reference</a>} } it 'links to a valid reference' do - doc = reference_filter("See #{reference}") + doc = reference_filter("See #{reference_link}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project2) end it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference_link}.)") expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end end context 'cross-project URL in link href' do + it_behaves_like 'a reference containing an element node' + let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:empty_project, :public, namespace: namespace) } let(:issue) { create(:issue, project: project2) } - let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} } + let(:reference) { "#{helper.url_for_issue(issue.iid, project2) + "#note_123"}" } + let(:reference_link) { %{<a href="#{reference}">Reference</a>} } it 'links to a valid reference' do - doc = reference_filter("See #{reference}") + doc = reference_filter("See #{reference_link}") expect(doc.css('a').first.attr('href')). to eq helper.url_for_issue(issue.iid, project2) + "#note_123" end it 'links with adjacent text' do - doc = reference_filter("Fixed (#{reference}.)") + doc = reference_filter("Fixed (#{reference_link}.)") expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 729e77fd43f..5bfeb82e738 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -24,6 +24,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end context 'mentioning @all' do + it_behaves_like 'a reference containing an element node' + let(:reference) { User.reference_prefix + 'all' } before do @@ -60,6 +62,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end context 'mentioning a user' do + it_behaves_like 'a reference containing an element node' + it 'links to a User' do doc = reference_filter("Hey #{reference}") expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) @@ -89,6 +93,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end context 'mentioning a group' do + it_behaves_like 'a reference containing an element node' + let(:group) { create(:group) } let(:reference) { group.to_reference } diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb new file mode 100644 index 00000000000..2501b638774 --- /dev/null +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +describe Banzai::Pipeline::FullPipeline do + describe 'References' do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project) } + + it 'handles markdown inside a reference' do + markdown = "[some `code` inside](#{issue.to_reference})" + result = described_class.call(markdown, project: project) + link_content = result[:output].css('a').inner_html + expect(link_content).to eq('some <code>code</code> inside') + end + + it 'sanitizes reference HTML' do + link_label = '<script>bad things</script>' + markdown = "[#{link_label}](#{issue.to_reference})" + result = described_class.to_html(markdown, project: project) + expect(result).not_to include(link_label) + end + + it 'escapes the data-original attribute on a reference' do + markdown = %Q{[">bad things](#{issue.to_reference})} + result = described_class.to_html(markdown, project: project) + expect(result).to include(%{data-original='\">bad things'}) + end + end +end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 254657a881d..6d2c141e18b 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -6,39 +6,60 @@ describe Banzai::Redactor do let(:redactor) { described_class.new(project, user) } describe '#redact' do - it 'redacts an Array of documents' do - doc1 = Nokogiri::HTML. - fragment('<a class="gfm" data-reference-type="issue">foo</a>') - - doc2 = Nokogiri::HTML. - fragment('<a class="gfm" data-reference-type="issue">bar</a>') - - expect(redactor).to receive(:nodes_visible_to_user).and_return([]) - - redacted_data = redactor.redact([doc1, doc2]) - - expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) - expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0]) - expect(doc1.to_html).to eq('foo') - expect(doc2.to_html).to eq('bar') + context 'when reference not visible to user' do + before do + expect(redactor).to receive(:nodes_visible_to_user).and_return([]) + end + + it 'redacts an array of documents' do + doc1 = Nokogiri::HTML. + fragment('<a class="gfm" data-reference-type="issue">foo</a>') + + doc2 = Nokogiri::HTML. + fragment('<a class="gfm" data-reference-type="issue">bar</a>') + + redacted_data = redactor.redact([doc1, doc2]) + + expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) + expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0]) + expect(doc1.to_html).to eq('foo') + expect(doc2.to_html).to eq('bar') + end + + it 'replaces redacted reference with inner HTML' do + doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>") + redactor.redact([doc]) + expect(doc.to_html).to eq('foo') + end + + context 'when data-original attribute provided' do + let(:original_content) { '<code>foo</code>' } + it 'replaces redacted reference with original content' do + doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>") + redactor.redact([doc]) + expect(doc.to_html).to eq(original_content) + end + end end - it 'does not redact an Array of documents' do - doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>' - doc1 = Nokogiri::HTML.fragment(doc1_html) + context 'when reference visible to user' do + it 'does not redact an array of documents' do + doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>' + doc1 = Nokogiri::HTML.fragment(doc1_html) - doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>' - doc2 = Nokogiri::HTML.fragment(doc2_html) + doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>' + doc2 = Nokogiri::HTML.fragment(doc2_html) - nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] } - expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten) + nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] } + expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten) - redacted_data = redactor.redact([doc1, doc2]) + redacted_data = redactor.redact([doc1, doc2]) - expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) - expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1]) - expect(doc1.to_html).to eq(doc1_html) - expect(doc2.to_html).to eq(doc2_html) + expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2]) + expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1]) + expect(doc1.to_html).to eq(doc1_html) + expect(doc2.to_html).to eq(doc2_html) + end end end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb new file mode 100644 index 00000000000..d5d87310874 --- /dev/null +++ b/spec/lib/gitlab/utils_spec.rb @@ -0,0 +1,35 @@ +describe Gitlab::Utils, lib: true do + def to_boolean(value) + described_class.to_boolean(value) + end + + describe '.to_boolean' do + it 'accepts booleans' do + expect(to_boolean(true)).to be(true) + expect(to_boolean(false)).to be(false) + end + + it 'converts a valid string to a boolean' do + expect(to_boolean(true)).to be(true) + expect(to_boolean('true')).to be(true) + expect(to_boolean('YeS')).to be(true) + expect(to_boolean('t')).to be(true) + expect(to_boolean('1')).to be(true) + expect(to_boolean('ON')).to be(true) + + expect(to_boolean('FaLse')).to be(false) + expect(to_boolean('F')).to be(false) + expect(to_boolean('NO')).to be(false) + expect(to_boolean('n')).to be(false) + expect(to_boolean('0')).to be(false) + expect(to_boolean('oFF')).to be(false) + end + + it 'converts an invalid string to nil' do + expect(to_boolean('fals')).to be_nil + expect(to_boolean('yeah')).to be_nil + expect(to_boolean('')).to be_nil + expect(to_boolean(nil)).to be_nil + end + end +end diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 5363aea4d22..9041690023f 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -22,4 +22,18 @@ describe ProjectFeaturesCompatibility do expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) end end + + it "converts fields from true to ProjectFeature::ENABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, true) + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) + end + end + + it "converts fields from false to ProjectFeature::DISABLED" do + features.each do |feature| + project.update_attribute("#{feature}_enabled".to_sym, false) + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f4dda1ee558..aef277357cf 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -67,11 +67,11 @@ describe Project, models: true do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } - context 'after create' do - it "creates project feature" do + context 'after initialized' do + it "has a project_feature" do project = FactoryGirl.build(:project) - expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true) + expect(project.project_feature.present?).to be_present end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 10c39b90212..d1ed774a914 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -599,6 +599,80 @@ describe User, models: true do end end + describe '.search_with_secondary_emails' do + def search_with_secondary_emails(query) + described_class.search_with_secondary_emails(query) + end + + let!(:user) { create(:user) } + let!(:email) { create(:email) } + + it 'returns users with a matching name' do + expect(search_with_secondary_emails(user.name)).to eq([user]) + end + + it 'returns users with a partially matching name' do + expect(search_with_secondary_emails(user.name[0..2])).to eq([user]) + end + + it 'returns users with a matching name regardless of the casing' do + expect(search_with_secondary_emails(user.name.upcase)).to eq([user]) + end + + it 'returns users with a matching email' do + expect(search_with_secondary_emails(user.email)).to eq([user]) + end + + it 'returns users with a partially matching email' do + expect(search_with_secondary_emails(user.email[0..2])).to eq([user]) + end + + it 'returns users with a matching email regardless of the casing' do + expect(search_with_secondary_emails(user.email.upcase)).to eq([user]) + end + + it 'returns users with a matching username' do + expect(search_with_secondary_emails(user.username)).to eq([user]) + end + + it 'returns users with a partially matching username' do + expect(search_with_secondary_emails(user.username[0..2])).to eq([user]) + end + + it 'returns users with a matching username regardless of the casing' do + expect(search_with_secondary_emails(user.username.upcase)).to eq([user]) + end + + it 'returns users with a matching whole secondary email' do + expect(search_with_secondary_emails(email.email)).to eq([email.user]) + end + + it 'returns users with a matching part of secondary email' do + expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user]) + end + + it 'return users with a matching part of secondary email regardless of case' do + expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user]) + expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user]) + expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user]) + end + + it 'returns multiple users with matching secondary emails' do + email1 = create(:email, email: '1_testemail@example.com') + email2 = create(:email, email: '2_testemail@example.com') + email3 = create(:email, email: 'other@email.com') + email3.user.update_attributes!(email: 'another@mail.com') + + expect( + search_with_secondary_emails('testemail@example.com').map(&:id) + ).to include(email1.user.id, email2.user.id) + + expect( + search_with_secondary_emails('testemail@example.com').map(&:id) + ).not_to include(email3.user.id) + end + end + describe 'by_username_or_id' do let(:user1) { create(:user, username: 'foo') } diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index f7fe4c10873..01bb9e955e0 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -265,36 +265,6 @@ describe API::Helpers, api: true do end end - describe '.to_boolean' do - it 'accepts booleans' do - expect(to_boolean(true)).to be(true) - expect(to_boolean(false)).to be(false) - end - - it 'converts a valid string to a boolean' do - expect(to_boolean(true)).to be(true) - expect(to_boolean('true')).to be(true) - expect(to_boolean('YeS')).to be(true) - expect(to_boolean('t')).to be(true) - expect(to_boolean('1')).to be(true) - expect(to_boolean('ON')).to be(true) - - expect(to_boolean('FaLse')).to be(false) - expect(to_boolean('F')).to be(false) - expect(to_boolean('NO')).to be(false) - expect(to_boolean('n')).to be(false) - expect(to_boolean('0')).to be(false) - expect(to_boolean('oFF')).to be(false) - end - - it 'converts an invalid string to nil' do - expect(to_boolean('fals')).to be_nil - expect(to_boolean('yeah')).to be_nil - expect(to_boolean('')).to be_nil - expect(to_boolean(nil)).to be_nil - end - end - describe '.handle_api_exception' do before do allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true) diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb new file mode 100644 index 00000000000..0670ac2faa2 --- /dev/null +++ b/spec/services/members/create_service_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Members::CreateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:project_user) { create(:user) } + + before { project.team << [user, :master] } + + it 'adds user to members' do + params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST } + result = described_class.new(project, user, params).execute + + expect(result).to be_truthy + expect(project.users).to include project_user + end + + it 'adds no user to members' do + params = { user_ids: '', access_level: Gitlab::Access::GUEST } + result = described_class.new(project, user, params).execute + + expect(result).to be_falsey + expect(project.users).not_to include project_user + end +end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 3a3f07ddcb9..3f5df049ea2 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -25,6 +25,8 @@ describe MergeRequests::BuildService, services: true do before do allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) + allow(project).to receive(:commit).and_return(commit_1) + allow(project).to receive(:commit).and_return(commit_2) end describe 'execute' do @@ -193,5 +195,52 @@ describe MergeRequests::BuildService, services: true do end end end + + context 'source branch does not exist' do + before do + allow(project).to receive(:commit).with(source_branch).and_return(nil) + allow(project).to receive(:commit).with(target_branch).and_return(commit_1) + end + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds an error message to the merge request' do + expect(merge_request.errors).to contain_exactly('Source branch "feature-branch" does not exist') + end + end + + context 'target branch does not exist' do + before do + allow(project).to receive(:commit).with(source_branch).and_return(commit_1) + allow(project).to receive(:commit).with(target_branch).and_return(nil) + end + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds an error message to the merge request' do + expect(merge_request.errors).to contain_exactly('Target branch "master" does not exist') + end + end + + context 'both source and target branches do not exist' do + before do + allow(project).to receive(:commit).and_return(nil) + end + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds both error messages to the merge request' do + expect(merge_request.errors).to contain_exactly( + 'Source branch "feature-branch" does not exist', + 'Target branch "master" does not exist' + ) + end + end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 3ea1273abc3..876bfaf085c 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do context 'wiki_enabled false does not create wiki repository directory' do before do - @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } }) + @opts.merge!(wiki_enabled: false) @project = create_project(@user, @opts) @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end diff --git a/spec/support/banzai/reference_filter_shared_examples.rb b/spec/support/banzai/reference_filter_shared_examples.rb new file mode 100644 index 00000000000..eb5da662ab5 --- /dev/null +++ b/spec/support/banzai/reference_filter_shared_examples.rb @@ -0,0 +1,13 @@ +# Specs for reference links containing HTML. +# +# Requires a reference: +# let(:reference) { '#42' } +shared_examples 'a reference containing an element node' do + let(:inner_html) { 'element <code>node</code> inside' } + let(:reference_with_element) { %{<a href="#{reference}">#{inner_html}</a>} } + + it 'does not escape inner html' do + doc = reference_filter(reference_with_element) + expect(doc.children.first.inner_html).to eq(inner_html) + end +end |