diff options
131 files changed, 1387 insertions, 536 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b8619d7d3f8..433b3119fba 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -240,7 +240,7 @@ rake db:seed_fu: paths: - log/development.log -karma: +rake karma: cache: paths: - vendor/ruby @@ -387,7 +387,7 @@ pages: <<: *dedicated-runner dependencies: - coverage - - karma + - rake karma - lint:javascript:report script: - mv public/ .public/ diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 0d91a54c7d4..9e11b32fcaa 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.3.0 +0.3.1 @@ -34,7 +34,7 @@ gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' -gem 'omniauth-authentiq', '~> 0.2.0' +gem 'omniauth-authentiq', '~> 0.3.0' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt', '~> 1.5.6' diff --git a/Gemfile.lock b/Gemfile.lock index a3c2fad41ba..4f98dc9d085 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -448,7 +448,7 @@ GEM rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) omniauth-oauth2 (~> 1.1) - omniauth-authentiq (0.2.2) + omniauth-authentiq (0.3.0) omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) @@ -925,7 +925,7 @@ DEPENDENCIES oj (~> 2.17.4) omniauth (~> 1.3.2) omniauth-auth0 (~> 1.4.1) - omniauth-authentiq (~> 0.2.0) + omniauth-authentiq (~> 0.3.0) omniauth-azure-oauth2 (~> 0.0.6) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) diff --git a/README.md b/README.md index 4f85fac4a56..09e08adbb73 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ We're hiring developers, support people, and production engineers all the time, There are two editions of GitLab: - GitLab Community Edition (CE) is available freely under the MIT Expat license. -- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). +- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/). ## Website diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index ffc7d29e4c5..13a9bf59246 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -173,7 +173,7 @@ tokens.forEach((token) => { const condition = gl.FilteredSearchTokenKeys .searchByConditionKeyValue(token.key, token.value.toLowerCase()); - const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); + const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key) || {}; const keyParam = param ? `${token.key}_${param}` : token.key; let tokenPath = ''; diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index cf53845a48b..9bf1b1ced88 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -1,9 +1,12 @@ +require('./filtered_search_token_keys'); + (() => { class FilteredSearchTokenizer { static processTokens(input) { + const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); // Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single) - const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; + const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokens = []; let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index bf3da8528f0..a01662e2f9e 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -47,9 +47,10 @@ } // Only filter asynchronously only if option remote is set if (this.options.remote) { - $inputContainer.parent().addClass('is-loading'); clearTimeout(timeout); return timeout = setTimeout(function() { + $inputContainer.parent().addClass('is-loading'); + return this.options.query(this.input.val(), function(data) { $inputContainer.parent().removeClass('is-loading'); return this.options.callback(data); diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 3945a789c82..78434b99b62 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -148,16 +148,11 @@ header { } .header-logo { - position: absolute; - left: 50%; + display: inline-block; + margin: 0 8px 0 3px; + position: relative; top: 7px; transition-duration: .3s; - z-index: 999; - - #logo { - position: relative; - left: -50%; - } svg, img { @@ -167,15 +162,6 @@ header { &:hover { cursor: pointer; } - - @media (max-width: $screen-xs-max) { - right: 20px; - left: auto; - - #logo { - left: auto; - } - } } .title { @@ -183,7 +169,7 @@ header { padding-right: 20px; margin: 0; font-size: 18px; - max-width: 385px; + max-width: 450px; display: inline-block; line-height: $header-height; font-weight: normal; @@ -193,10 +179,6 @@ header { vertical-align: top; white-space: nowrap; - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - max-width: 300px; - } - @media (max-width: $screen-xs-max) { max-width: 190px; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 674d3bb45aa..7d4a814a36c 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,7 +1,7 @@ @mixin fade($gradient-direction, $gradient-color) { visibility: hidden; opacity: 0; - z-index: 2; + z-index: 1; position: absolute; bottom: 12px; width: 43px; @@ -18,7 +18,7 @@ .fa { position: relative; - top: 5px; + top: 6px; font-size: 18px; } } @@ -79,7 +79,6 @@ } &.sub-nav { - text-align: center; background-color: $gray-normal; .container-fluid { @@ -287,7 +286,6 @@ background: $gray-light; border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; - text-align: center; .container-fluid { position: relative; @@ -353,7 +351,7 @@ right: -5px; .fa { - right: -7px; + right: -28px; } } @@ -383,7 +381,7 @@ left: 0; .fa { - left: 10px; + left: -4px; } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 00eb5b30fd5..3fe1eef307e 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -222,6 +222,11 @@ } } + .dropdown-menu { + max-height: 250px; + overflow-y: auto; + } + .dropdown-toggle, .dropdown-menu { color: $gl-text-color-secondary; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6b05e5bb4aa..8c0de314420 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -268,6 +268,13 @@ } } +.project-repo-buttons { + .project-action-button .dropdown-menu { + max-height: 250px; + overflow-y: auto; + } +} + .split-one { display: inline-table; margin-right: 12px; @@ -645,29 +652,23 @@ pre.light-well { } } -.project-last-commit { - @media (min-width: $screen-sm-min) { - margin-top: $gl-padding; +.container-fluid.project-stats-container { + @media (max-width: $screen-xs-max) { + padding: 12px 0; } +} - &.container-fluid { - padding-top: 12px; - padding-bottom: 12px; - background-color: $gray-light; - border: 1px solid $border-color; - border-right-width: 0; - border-left-width: 0; +.project-last-commit { + background-color: $gray-light; + padding: 12px $gl-padding; + border: 1px solid $border-color; - @media (min-width: $screen-sm-min) { - border-right-width: 1px; - border-left-width: 1px; - } + @media (min-width: $screen-sm-min) { + margin-top: $gl-padding; } - &.container-limited { - @media (min-width: 1281px) { - border-radius: $border-radius-base; - } + @media (min-width: $screen-sm-min) { + border-radius: $border-radius-base; } .ci-status { diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 551a66fbf3a..bac831ec315 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -43,6 +43,12 @@ } } + .todo-avatar, + .todo-actions { + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + } + .todo-actions { display: -webkit-flex; display: flex; @@ -55,8 +61,9 @@ } .todo-item { - -webkit-flex: auto; - flex: auto; + -webkit-flex: 0 1 100%; + flex: 0 1 100%; + min-width: 0; } } @@ -74,8 +81,29 @@ .todo-item { .todo-title { - @include str-truncated(calc(100% - 174px)); - overflow: visible; + display: flex; + + & > .title-item { + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + margin: 0 2px; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + + .todo-label { + -webkit-flex: 0 1 auto; + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } .status-box { @@ -154,10 +182,12 @@ .todo-item { .todo-title { - white-space: normal; - overflow: visible; - max-width: 100%; + flex-flow: row wrap; margin-bottom: 10px; + + .todo-label { + white-space: normal; + } } .todo-body { diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb index 338496013a0..c09095b9849 100644 --- a/app/controllers/admin/background_jobs_controller.rb +++ b/app/controllers/admin/background_jobs_controller.rb @@ -2,5 +2,6 @@ class Admin::BackgroundJobsController < Admin::ApplicationController def show ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/) + @concurrency = Sidekiq.options[:concurrency] end end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 6286d67d30c..88d180fcc2e 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -104,23 +104,15 @@ module CreatesCommit if can?(current_user, :push_code, @project) # Edit file in this project @mr_source_project = @project - - if @project.forked? - # Merge request from this project to fork origin - @mr_target_project = @project.forked_from_project - @mr_target_branch = @mr_target_project.repository.root_ref - else - # Merge request to this project - @mr_target_project = @project - @mr_target_branch = @ref || @target_branch - end else # Merge request from fork to this project @mr_source_project = current_user.fork_of(@project) - @mr_target_project = @project - @mr_target_branch = @ref || @target_branch end + # Merge request to this project + @mr_target_project = @project + @mr_target_branch = @ref || @target_branch + @mr_source_branch = guess_mr_source_branch end diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb new file mode 100644 index 00000000000..ca6dffe1cc5 --- /dev/null +++ b/app/controllers/concerns/snippets_actions.rb @@ -0,0 +1,21 @@ +module SnippetsActions + extend ActiveSupport::Concern + + def edit + end + + def raw + send_data( + convert_line_endings(@snippet.content), + type: 'text/plain; charset=utf-8', + disposition: 'inline', + filename: @snippet.sanitized_file_name + ) + end + + private + + def convert_line_endings(content) + params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n") + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f54c79c2e37..3ab7e6e0658 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -78,6 +78,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController handle_omniauth end + def authentiq + if params['sid'] + handle_service_ticket oauth['provider'], params['sid'] + end + handle_omniauth + end + private def handle_omniauth diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 5d193f26a8e..ef5d3d242eb 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,6 +1,7 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji include SpammableActions + include SnippetsActions before_action :module_enabled before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] @@ -49,9 +50,6 @@ class Projects::SnippetsController < Projects::ApplicationController end end - def edit - end - def update UpdateSnippetService.new(project, current_user, @snippet, snippet_params).execute @@ -74,15 +72,6 @@ class Projects::SnippetsController < Projects::ApplicationController redirect_to namespace_project_snippets_path(@project.namespace, @project) end - def raw - send_data( - @snippet.content, - type: 'text/plain; charset=utf-8', - disposition: 'inline', - filename: @snippet.sanitized_file_name - ) - end - protected def snippet diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index b169d993688..366804ab17e 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,6 +1,7 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji include SpammableActions + include SnippetsActions before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] @@ -47,9 +48,6 @@ class SnippetsController < ApplicationController respond_with @snippet.becomes(Snippet) end - def edit - end - def update UpdateSnippetService.new(nil, current_user, @snippet, snippet_params).execute @@ -67,18 +65,9 @@ class SnippetsController < ApplicationController redirect_to snippets_path end - def raw - send_data( - @snippet.content, - type: 'text/plain; charset=utf-8', - disposition: 'inline', - filename: @snippet.sanitized_file_name - ) - end - def download send_data( - @snippet.content, + convert_line_endings(@snippet.content), type: 'text/plain; charset=utf-8', filename: @snippet.sanitized_file_name ) diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 0676767d910..dc5ae8edbb2 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -1,4 +1,8 @@ module NamespacesHelper + def namespace_id_from(params) + params.dig(:project, :namespace_id) || params[:namespace_id] + end + def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) groups = current_user.owned_groups + current_user.masters_groups diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 423ae98a60e..79adc77c9e4 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -22,6 +22,10 @@ module Spammable delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true end + def submittable_as_spam_by?(current_user) + current_user && current_user.admin? && submittable_as_spam? + end + def submittable_as_spam? if user_agent_detail user_agent_detail.submittable? && current_application_settings.akismet_enabled diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index a08c6fcd94b..9716a1780a9 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -17,8 +17,6 @@ module Projects def execute return false unless can?(current_user, :remove_project, project) - project.team.truncate - repo_path = project.path_with_namespace wiki_path = repo_path + '.wiki' @@ -30,6 +28,7 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute Project.transaction do + project.team.truncate project.destroy! unless remove_registry_tags diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 2d11305be13..bc0653cb634 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -7,6 +7,10 @@ module Users end def execute(user, options = {}) + unless current_user.admin? || current_user == user + raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!" + end + if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present? user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' return user diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 4f982a6e369..ac36bb5bb17 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -35,7 +35,7 @@ .clearfix %p %i.fa.fa-exclamation-circle - If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'. + If '[#{@concurrency} of #{@concurrency} busy]' is shown, restart GitLab with 'sudo service gitlab reload'. %p %i.fa.fa-exclamation-circle If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab. diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index dc2d924f212..a3993d5ef16 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,28 +1,33 @@ %li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } } - = author_avatar(todo, size: 40) + .todo-avatar + = author_avatar(todo, size: 40) .todo-item.todo-block .todo-title.title - unless todo.build_failed? || todo.unmergeable? = todo_target_state_pill(todo) - %span.author-name + .title-item.author-name - if todo.author = link_to_author(todo) - else (removed) - %span.action-name + .title-item.action-name = todo_action_name(todo) - %span.todo-label + .title-item.todo-label - if todo.target = todo_target_link(todo) - else (removed) - · #{time_ago_with_tooltip(todo.created_at)} - = todo_due_date(todo) + .title-item + · + + .title-item + #{time_ago_with_tooltip(todo.created_at)} + = todo_due_date(todo) .todo-body .todo-note diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 64ca3c32e01..efd13aabf20 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -3,11 +3,9 @@ .event-title %span.author_name= link_to_author event %span.pushed #{event.action_name} #{event.ref_type} - - if event.rm_ref? - %strong= event.ref_name - - else - %strong - = link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title) + %strong + - commits_link = namespace_project_commits_path(project.namespace, project, event.ref_name) + = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link = render "events/event_scope", event: event diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index a35a918d501..1717ed6b365 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,7 +1,7 @@ .page-with-sidebar{ class: page_gutter_class } - if defined?(nav) && nav .layout-nav - .container-fluid + %div{ class: container_class } = render "layouts/nav/#{nav}" .content-wrapper{ class: "#{layout_nav_class}" } = yield :sub_nav diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index ddf50d6667f..60b9b8bdbc4 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -61,12 +61,12 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - %h1.title= title - .header-logo = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo + %h1.title= title + = yield :header_content = render 'shared/outdated_browser' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index d3eb3b7055b..069f3d97943 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -40,7 +40,7 @@ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, 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? + - if @issue.submittable_as_spam_by?(current_user) %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' @@ -50,7 +50,7 @@ - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, 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 }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - - if @issue.submittable_as_spam? && current_user.admin? + - if @issue.submittable_as_spam_by?(current_user) = 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' diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a07885537b9..2a98bba05ee 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -22,7 +22,7 @@ - if current_user.can_select_namespace? .input-group-addon = root_url - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} + = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} - else .input-group-addon.static-namespace diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 8024fb8979d..132f6372e40 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -60,7 +60,7 @@ = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' .input-group %span.input-group-addon / - = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression' %span.input-group-addon / %p.help-block A regular expression that will be used to find the test coverage diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 80d4081dd7b..f7419728719 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,69 +13,70 @@ = render "home_panel" - if current_user && can?(current_user, :download_code, @project) - %nav.project-stats{ class: container_class } - %ul.nav - %li - = link_to project_files_path(@project) do - Files (#{storage_counter(@project.statistics.total_repository_size)}) - %li - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) - %li - = link_to namespace_project_branches_path(@project.namespace, @project) do - #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) - %li - = link_to namespace_project_tags_path(@project.namespace, @project) do - #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - - if default_project_view != 'readme' && @repository.readme + .project-stats-container{ class: container_class } + %nav.project-stats + %ul.nav %li - = link_to 'Readme', readme_path(@project) - - - if @repository.changelog + = link_to project_files_path(@project) do + Files (#{storage_counter(@project.statistics.total_repository_size)}) %li - = link_to 'Changelog', changelog_path(@project) - - - if @repository.license_blob + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)}) %li - = link_to license_short_name(@project), license_path(@project) - - - if @repository.contribution_guide + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) %li - = link_to 'Contribution guide', contribution_guide_path(@project) + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)}) - - if @repository.gitlab_ci_yml - %li - = link_to 'CI configuration', ci_configuration_path(@project) + - if default_project_view != 'readme' && @repository.readme + %li + = link_to 'Readme', readme_path(@project) + + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) + + - if @repository.license_blob + %li + = link_to license_short_name(@project), license_path(@project) + + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) + + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', ci_configuration_path(@project) - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide - - unless @repository.gitlab_ci_yml - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do - Set up CI - - if koding_enabled? && @repository.koding_yml.blank? - %li.missing - = link_to 'Set up Koding', add_koding_stack_path(@project) - - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? - %li.missing - = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do - Set up auto deploy + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do + Add Changelog + - unless @repository.license_blob + %li.missing + = link_to add_special_file_path(@project, file_name: 'LICENSE') do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do + Add Contribution guide + - unless @repository.gitlab_ci_yml + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do + Set up CI + - if koding_enabled? && @repository.koding_yml.blank? + %li.missing + = link_to 'Set up Koding', add_koding_stack_path(@project) + - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? + %li.missing + = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do + Set up auto deploy - - if @repository.commit - .project-last-commit{ class: container_class } - = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project + - if @repository.commit + .project-last-commit + = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project %div{ class: container_class } - if @project.archived? diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index dde2e2b644d..34ee4ff1937 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -10,7 +10,7 @@ - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown @@ -31,6 +31,6 @@ %li = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do Edit - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index e977c1f1698..f84be600df8 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -12,41 +12,34 @@ %span.light= time_ago_with_tooltip(snippet.created_at) %h4.snippet-title - snippet_path = reliable_snippet_path(snippet) - = link_to snippet_path do - .file-holder - .js-file-title.file-title + .file-holder + .js-file-title.file-title + = link_to snippet_path do %i.fa.fa-file %strong= snippet.file_name - - if markup?(snippet.file_name) - .file-content.wiki + - if markup?(snippet.file_name) + .file-content.wiki + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + = render_markup(snippet.file_name, chunk[:data]) + - else + .file-content.code + .nothing-here-block Empty file + - else + .file-content.code.js-syntax-highlight + .line-numbers - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = render_markup(snippet.file_name, chunk[:data]) + - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index| + - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1 + - i = index + offset + = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do + %i.fa.fa-link + = i + .blob-content + - snippet_chunks.each do |chunk| + - unless chunk[:data].empty? + = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.no_highlighting?) - else .file-content.code .nothing-here-block Empty file - - else - .file-content.code.js-syntax-highlight - .line-numbers - - snippet_chunks.each do |chunk| - - unless chunk[:data].empty? - - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index| - - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1 - - i = index + offset - = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do - %i.fa.fa-link - = i - - unless snippet == snippet_chunks.last - %a.diff-line-num - = "." - %pre.code - %code - - snippet_chunks.each do |chunk| - - unless chunk[:data].empty? - = chunk[:data] - - unless chunk == snippet_chunks.last - %a - = "..." - - else - .file-content.code - .nothing-here-block Empty file diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 239387fc9fa..8e721c9c8dd 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -61,7 +61,7 @@ = dropdown_title("Change permissions") .dropdown-content %ul - - Gitlab::Access.options.each do |role, role_id| + - member.class.access_level_roles.each do |role, role_id| %li = link_to role, "javascript:void(0)", class: ("is-active" if member.access_level == role_id), diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 855a995afa9..a7f118d3f7d 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -9,7 +9,7 @@ Delete = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -28,6 +28,6 @@ %li = link_to edit_snippet_path(@snippet) do Edit - - if @snippet.submittable_as_spam? && current_user.admin? + - if @snippet.submittable_as_spam_by?(current_user) %li = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb index 5483bbb210b..3340a7be4fe 100644 --- a/app/workers/delete_user_worker.rb +++ b/app/workers/delete_user_worker.rb @@ -7,5 +7,7 @@ class DeleteUserWorker current_user = User.find(current_user_id) Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys) + rescue Gitlab::Access::AccessDeniedError => e + Rails.logger.warn("User could not be destroyed: #{e}") end end diff --git a/changelogs/unreleased/21240_snippets_line_ending.yml b/changelogs/unreleased/21240_snippets_line_ending.yml new file mode 100644 index 00000000000..880fdd2c9ed --- /dev/null +++ b/changelogs/unreleased/21240_snippets_line_ending.yml @@ -0,0 +1,4 @@ +--- +title: Download snippets with LF line-endings by default +merge_request: 8999 +author: diff --git a/changelogs/unreleased/26206-fix-download-dropdown.yml b/changelogs/unreleased/26206-fix-download-dropdown.yml new file mode 100644 index 00000000000..a6c101375bb --- /dev/null +++ b/changelogs/unreleased/26206-fix-download-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Set dropdown height fixed to 250px and make it scrollable +merge_request: 9063 +author: diff --git a/changelogs/unreleased/26315-unify-labels-filter-behavior.yml b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml new file mode 100644 index 00000000000..cd2f40c94fe --- /dev/null +++ b/changelogs/unreleased/26315-unify-labels-filter-behavior.yml @@ -0,0 +1,4 @@ +--- +title: Unify issues search behavior by always filtering when ALL labels matches +merge_request: 8849 +author: diff --git a/changelogs/unreleased/27934-left-align-nav.yml b/changelogs/unreleased/27934-left-align-nav.yml new file mode 100644 index 00000000000..6c45e4ce175 --- /dev/null +++ b/changelogs/unreleased/27934-left-align-nav.yml @@ -0,0 +1,4 @@ +--- +title: Left align navigation +merge_request: +author: diff --git a/changelogs/unreleased/28082-deleted-branch-event-404.yml b/changelogs/unreleased/28082-deleted-branch-event-404.yml new file mode 100644 index 00000000000..e989ca34784 --- /dev/null +++ b/changelogs/unreleased/28082-deleted-branch-event-404.yml @@ -0,0 +1,4 @@ +--- +title: Stop linking to deleted Branches in Activity tabs +merge_request: 9203 +author: Jan Christophersen diff --git a/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml b/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml new file mode 100644 index 00000000000..3bcf0e06d08 --- /dev/null +++ b/changelogs/unreleased/28186-long-group-names-overflow-out-of-todos-view.yml @@ -0,0 +1,4 @@ +--- +title: Truncate long Todo titles for non-mobile screens +merge_request: 9311 +author: diff --git a/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml new file mode 100644 index 00000000000..df2478a3f28 --- /dev/null +++ b/changelogs/unreleased/28204-option-to-disable-webpack-dev-server-livereload.yml @@ -0,0 +1,4 @@ +--- +title: Pick up option from GDK to disable webpack dev server livereload +merge_request: +author: diff --git a/changelogs/unreleased/28357-colon-search.yml b/changelogs/unreleased/28357-colon-search.yml new file mode 100644 index 00000000000..4bbb0dc12b2 --- /dev/null +++ b/changelogs/unreleased/28357-colon-search.yml @@ -0,0 +1,4 @@ +--- +title: Allow searching issues for strings containing colons +merge_request: +author: diff --git a/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml b/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml new file mode 100644 index 00000000000..ed357d86fe3 --- /dev/null +++ b/changelogs/unreleased/28389-ux-problem-with-pipeline-coverage-placeholder.yml @@ -0,0 +1,4 @@ +--- +title: Changed coverage reg expression placeholder text to be more like a placeholder +merge_request: +author: diff --git a/changelogs/unreleased/9381-authentiq-backchannel-logout.yml b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml new file mode 100644 index 00000000000..4dbf36cd096 --- /dev/null +++ b/changelogs/unreleased/9381-authentiq-backchannel-logout.yml @@ -0,0 +1,4 @@ +--- +title: Adds remote logout functionality to the Authentiq OAuth provider +merge_request: 9381 +author: Alexandros Keramidas diff --git a/changelogs/unreleased/api-post-block.yml b/changelogs/unreleased/api-post-block.yml new file mode 100644 index 00000000000..dfc61ffa9e3 --- /dev/null +++ b/changelogs/unreleased/api-post-block.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Use POST to (un)block a user' +merge_request: 9371 +author: Robert Schilling diff --git a/changelogs/unreleased/api-remove-deploy-key-disable.yml b/changelogs/unreleased/api-remove-deploy-key-disable.yml new file mode 100644 index 00000000000..f471ad2aa20 --- /dev/null +++ b/changelogs/unreleased/api-remove-deploy-key-disable.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Remove `DELETE projects/:id/deploy_keys/:key_id/disable`' +merge_request: 9365 +author: Robert Schilling diff --git a/changelogs/unreleased/api-star-restful.yml b/changelogs/unreleased/api-star-restful.yml new file mode 100644 index 00000000000..3e7de8cd822 --- /dev/null +++ b/changelogs/unreleased/api-star-restful.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`' +merge_request: 9328 +author: Robert Schilling diff --git a/changelogs/unreleased/api-todos-restful.yml b/changelogs/unreleased/api-todos-restful.yml new file mode 100644 index 00000000000..dba1350a495 --- /dev/null +++ b/changelogs/unreleased/api-todos-restful.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Use POST requests to mark todos as done' +merge_request: 9410 +author: Robert Schilling diff --git a/changelogs/unreleased/artifactsdoc.yml b/changelogs/unreleased/artifactsdoc.yml new file mode 100644 index 00000000000..4ef32d5256f --- /dev/null +++ b/changelogs/unreleased/artifactsdoc.yml @@ -0,0 +1,4 @@ +--- +title: Added documentation for permalinks to most recent build artifacts. +merge_request: 8934 +author: Christian Godenschwager diff --git a/changelogs/unreleased/feature-github-find-users-by-email.yml b/changelogs/unreleased/feature-github-find-users-by-email.yml new file mode 100644 index 00000000000..1503cf2b9f7 --- /dev/null +++ b/changelogs/unreleased/feature-github-find-users-by-email.yml @@ -0,0 +1,4 @@ +--- +title: GitHub Importer - Find users based on GitHub email address +merge_request: 8958 +author: diff --git a/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml new file mode 100644 index 00000000000..e09d03bb608 --- /dev/null +++ b/changelogs/unreleased/fix_sidekiq_concurrency_warning_message_in_admin_background_job_page.yml @@ -0,0 +1,4 @@ +--- +title: fix incorrect sidekiq concurrency count in admin background page +merge_request: +author: wendy0402 diff --git a/changelogs/unreleased/group-memebrs-owner-level.yml b/changelogs/unreleased/group-memebrs-owner-level.yml new file mode 100644 index 00000000000..ba77f38eb6d --- /dev/null +++ b/changelogs/unreleased/group-memebrs-owner-level.yml @@ -0,0 +1,4 @@ +--- +title: Added option to update to owner for group members +merge_request: +author: diff --git a/changelogs/unreleased/sh-delete-user-permission-check.yml b/changelogs/unreleased/sh-delete-user-permission-check.yml new file mode 100644 index 00000000000..c0e79aae2a8 --- /dev/null +++ b/changelogs/unreleased/sh-delete-user-permission-check.yml @@ -0,0 +1,4 @@ +--- +title: Add user deletion permission check in `Users::DestroyService` +merge_request: +author: diff --git a/changelogs/unreleased/snippets-search.yml b/changelogs/unreleased/snippets-search.yml new file mode 100644 index 00000000000..00cf34f4a48 --- /dev/null +++ b/changelogs/unreleased/snippets-search.yml @@ -0,0 +1,4 @@ +--- +title: Fix snippets search result spacing +merge_request: +author: diff --git a/changelogs/unreleased/updated-pages-0-3-1.yml b/changelogs/unreleased/updated-pages-0-3-1.yml new file mode 100644 index 00000000000..8622b823c86 --- /dev/null +++ b/changelogs/unreleased/updated-pages-0-3-1.yml @@ -0,0 +1,4 @@ +--- +title: Update GitLab Pages to v0.3.1 +merge_request: +author: diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a8afc36fc78..738dbeefc11 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -240,6 +240,17 @@ Devise.setup do |config| true end end + if provider['name'] == 'authentiq' + provider['args'][:remote_sign_out_handler] = lambda do |request| + authentiq_session = request.params['sid'] + if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session) + Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session) + true + else + false + end + end + end if provider['name'] == 'shibboleth' provider['args'][:fail_with_empty_uid] = true diff --git a/config/webpack.config.js b/config/webpack.config.js index f71bd686136..15899993874 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -10,6 +10,7 @@ var ROOT_PATH = path.resolve(__dirname, '..'); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; +var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), @@ -84,8 +85,7 @@ var config = { 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), - 'vue$': 'vue/dist/vue.js', - 'vue-resource$': 'vue-resource/dist/vue-resource.js' + 'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js', } } } @@ -115,6 +115,7 @@ if (IS_DEV_SERVER) { port: DEV_SERVER_PORT, headers: { 'Access-Control-Allow-Origin': '*' }, stats: 'errors-only', + inline: DEV_SERVER_LIVERELOAD }; config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; } diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb new file mode 100644 index 00000000000..c01753cfbd2 --- /dev/null +++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexToUserAgentDetail < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index(:user_agent_details, [:subject_id, :subject_type]) + end +end diff --git a/db/schema.rb b/db/schema.rb index e0208dab3d3..88aaa6c3c55 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1218,6 +1218,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do t.datetime "updated_at", null: false end + add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false diff --git a/doc/README.md b/doc/README.md index 1943d656aa7..2712206373d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -21,6 +21,7 @@ - [Profile Settings](profile/README.md) - [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. +- [Snippets](user/snippets.md) Snippets allow you to create little bits of code. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md index 3f39539da95..fb1a16b0f96 100644 --- a/doc/administration/auth/authentiq.md +++ b/doc/administration/auth/authentiq.md @@ -54,7 +54,7 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t 5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits. See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers. -6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1. +6. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in step 1. 7. Save the configuration file. diff --git a/doc/api/commits.md b/doc/api/commits.md index 3223b82f60a..eaab3e0df3d 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -12,8 +12,8 @@ GET /projects/:id/repository/commits | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | -| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | -| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | +| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | +| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 284d5f88c55..39afc4b2df5 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -137,7 +137,7 @@ Example response: ## Delete deploy key -Delete a deploy key from a project +Removes a deploy key from the project. If the deploy key is used only for this project, it will be deleted from the system. ``` DELETE /projects/:id/deploy_keys/:key_id @@ -156,14 +156,11 @@ Example response: ```json { - "updated_at" : "2015-08-29T12:50:57.259Z", - "key" : "ssh-rsa AAAA...", - "public" : false, - "title" : "My deploy key", - "user_id" : null, - "created_at" : "2015-08-29T12:50:57.259Z", - "fingerprint" : "6a:33:1f:74:51:c0:39:81:79:ec:7a:31:f8:40:20:43", - "id" : 13 + "id": 6, + "deploy_key_id": 14, + "project_id": 1, + "created_at" : "2015-08-29T12:50:57.259Z", + "updated_at" : "2015-08-29T12:50:57.259Z" } ``` @@ -190,27 +187,3 @@ Example response: "created_at" : "2015-08-29T12:44:31.550Z" } ``` - -## Disable a deploy key - -Disable a deploy key for a project. Returns the disabled key. - -```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID of the project | -| `key_id` | integer | yes | The ID of the deploy key | - -Example response: - -```json -{ - "key" : "ssh-rsa AAAA...", - "id" : 12, - "title" : "My deploy key", - "created_at" : "2015-08-29T12:44:31.550Z" -} -``` diff --git a/doc/api/issues.md b/doc/api/issues.md index 7c0a444d4fa..f40e0938b0f 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -30,7 +30,7 @@ GET /issues?milestone=1.0.0&state=opened | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| -| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | | `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | @@ -188,7 +188,7 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened | `id` | integer | yes | The ID of a project | | `iid` | integer | no | Return the issue having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed`| -| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned | | `milestone` | string| no | The milestone title | | `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | diff --git a/doc/api/projects.md b/doc/api/projects.md index b3136be6731..e9ef03a0c0c 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -609,7 +609,7 @@ Example response: Unstars a given project. Returns status code `304` if the project is not starred. ``` -DELETE /projects/:id/star +POST /projects/:id/unstar ``` | Attribute | Type | Required | Description | @@ -617,7 +617,7 @@ DELETE /projects/:id/star | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar" ``` Example response: @@ -1194,4 +1194,4 @@ Parameters: | --------- | ---- | -------- | ----------- | | `query` | string | yes | A string contained in the project name | | `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | -| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
\ No newline at end of file +| `sort` | string | no | Return requests sorted in `asc` or `desc` order | diff --git a/doc/api/todos.md b/doc/api/todos.md index a5e81801024..a2fbbc7e1f8 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -184,7 +184,7 @@ Marks a single pending todo given by its ID for the current user as done. The todo marked as done is returned in the response. ``` -DELETE /todos/:id +POST /todos/:id/mark_as_done ``` Parameters: @@ -194,7 +194,7 @@ Parameters: | `id` | integer | yes | The ID of a todo | ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130 +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130/mark_as_done ``` Example Response: @@ -277,20 +277,15 @@ Example Response: ## Mark all todos as done -Marks all pending todos for the current user as done. It returns the number of marked todos. +Marks all pending todos for the current user as done. It returns the HTTP status code `204` with an empty response. ``` -DELETE /todos +POST /todos/mark_as_done ``` ```bash -curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/donmark_as_donee ``` -Example Response: - -```json -3 -``` [ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188 diff --git a/doc/api/users.md b/doc/api/users.md index 626f7e63e3e..852c7ac8ec2 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -659,14 +659,14 @@ Will return `200 OK` on success, or `404 Not found` if either user or email cann Blocks the specified user. Available only for admin. ``` -PUT /users/:id/block +POST /users/:id/block ``` Parameters: - `id` (required) - id of specified user -Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +Will return `201 OK` on success, `404 User Not Found` is user cannot be found or `403 Forbidden` when trying to block an already blocked user by LDAP synchronization. ## Unblock user @@ -674,14 +674,14 @@ Will return `200 OK` on success, `404 User Not Found` is user cannot be found or Unblocks the specified user. Available only for admin. ``` -PUT /users/:id/unblock +POST /users/:id/unblock ``` Parameters: - `id` (required) - id of specified user -Will return `200 OK` on success, `404 User Not Found` is user cannot be found or +Will return `201 OK` on success, `404 User Not Found` is user cannot be found or `403 Forbidden` when trying to unblock a user blocked by LDAP synchronization. ### Get user contribution events diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 5c1fa6b47a0..1af124c56b1 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -13,6 +13,7 @@ changes are in V4: - Project snippets do not return deprecated field `expires_at` - Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) - Status 409 returned for POST `project/:id/members` when a member already exists +- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` - Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) - `/licences` - `/licences/:key` @@ -23,5 +24,9 @@ changes are in V4: - `/gitlab_ci_ymls/:key` - `/dockerfiles/:key` - Moved `/projects/fork/:id` to `/projects/:id/fork` +- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` - Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters - Return pagination headers for all endpoints that return an array +- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead +- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` +- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 9dee61bfa1f..00787323b6b 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -39,13 +39,15 @@ accessible during the build process. ## What is an image -The `image` keyword is the name of the docker image that is present in the -local Docker Engine (list all images with `docker images`) or any image that -can be found at [Docker Hub][hub]. For more information about images and Docker -Hub please read the [Docker Fundamentals][] documentation. +The `image` keyword is the name of the docker image the docker executor +will run to perform the CI tasks. -In short, with `image` we refer to the docker image, which will be used to -create a container on which your job will run. +By default the executor will only pull images from [Docker Hub][hub], +but this can be configured in the `gitlab-runner/config.toml` by setting +the [docker pull policy][] to allow using local images. + +For more information about images and Docker Hub please read +the [Docker Fundamentals][] documentation. ## What is a service @@ -271,6 +273,7 @@ containers as well as all volumes (`-v`) that were created with the container creation. [Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/ +[docker pull policy]: https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work [hub]: https://hub.docker.com/ [linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/ [tutum/wordpress]: https://hub.docker.com/r/tutum/wordpress/ diff --git a/doc/development/licensing.md b/doc/development/licensing.md index 5d177eb26ee..1f115059fb8 100644 --- a/doc/development/licensing.md +++ b/doc/development/licensing.md @@ -64,6 +64,10 @@ Libraries with the following licenses are unacceptable for use: - [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]. +## Requesting Approval for Licenses + +Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please create an issue in the [Organization Repository][Org-Repo] and cc `@gl-legal`. After a decision has been made, the original requestor is responsible for updating this document. + ## Notes Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL. @@ -96,3 +100,6 @@ Gems which are included only in the "development" or "test" groups by Bundler ar [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 +[Org-Repo]: https://gitlab.com/gitlab-com/organization +[Acceptable-Licenses]: #acceptable-licenses +[Unacceptable-Licenses]: #unacceptable-licenses diff --git a/doc/development/testing.md b/doc/development/testing.md index 761847b2bab..9b545d7f0f1 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -95,6 +95,25 @@ so we need to set some guidelines for their use going forward: [lets-not]: https://robots.thoughtbot.com/lets-not +### Time-sensitive tests + +[Timecop](https://github.com/travisjeffery/timecop) is available in our +Ruby-based tests for verifying things that are time-sensitive. Any test that +exercises or verifies something time-sensitive should make use of Timecop to +prevent transient test failures. + +Example: + +```ruby +it 'is overdue' do + issue = build(:issue, due_date: Date.tomorrow) + + Timecop.freeze(3.days.from_now) do + expect(issue).to be_overdue + end +end +``` + ### Test speed GitLab has a massive test suite that, without parallelization, can take more @@ -115,6 +134,10 @@ Here are some things to keep in mind regarding test performance: ### Features / Integration +GitLab uses [rspec-rails feature specs] to test features in a browser +environment. These are [capybara] specs running on the headless [poltergeist] +driver. + - Feature specs live in `spec/features/` and should be named `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`. - Use only one `feature` block per feature spec file. @@ -122,6 +145,10 @@ Here are some things to keep in mind regarding test performance: - Avoid scenario titles that add no information, such as "successfully." - Avoid scenario titles that repeat the feature title. +[rspec-rails feature specs]: https://github.com/rspec/rspec-rails#feature-specs +[capybara]: https://github.com/teamcapybara/capybara +[poltergeist]: https://github.com/teampoltergeist/poltergeist + ## Spinach (feature) tests GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index 205b3e8a835..ead79ba6a10 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -176,4 +176,4 @@ Portions of this page are modifications based on work created and shared by the [products]: https://about.gitlab.com/products/ "GitLab products page"
[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
[android project]: http://source.android.com/
-[creative commons]: http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file +[creative commons]: http://creativecommons.org/licenses/by/2.5/
diff --git a/doc/development/ux_guide/img/harry-robison.png b/doc/development/ux_guide/img/harry-robison.png Binary files differnew file mode 100644 index 00000000000..702a8b02262 --- /dev/null +++ b/doc/development/ux_guide/img/harry-robison.png diff --git a/doc/development/ux_guide/img/james-mackey.png b/doc/development/ux_guide/img/james-mackey.png Binary files differnew file mode 100644 index 00000000000..6db257c5b39 --- /dev/null +++ b/doc/development/ux_guide/img/james-mackey.png diff --git a/doc/development/ux_guide/img/steven-lyons.png b/doc/development/ux_guide/img/steven-lyons.png Binary files differnew file mode 100644 index 00000000000..2efe1d0b168 --- /dev/null +++ b/doc/development/ux_guide/img/steven-lyons.png diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md index 717a902c424..da410a8de7a 100644 --- a/doc/development/ux_guide/users.md +++ b/doc/development/ux_guide/users.md @@ -1,16 +1,164 @@ -# Users +## UX Personas +* [Nazim Ramesh](#nazim-ramesh) + - Small to medium size organisations using GitLab CE +* [James Mackey](#james-mackey) + - Medium to large size organisations using CE or EE + - Small organisations using EE +* [Karolina Plaskaty](#karolina-plaskaty) + - Using GitLab.com for personal/hobby projects + - Would like to use GitLab at work + - Working for a medium to large size organisation -> TODO: Create personas. Understand the similarities and differences across the below spectrums. +<hr> -## Users by organization +### Nazim Ramesh +- Small to medium size organisations using GitLab CE -- Enterprise -- Medium company -- Small company -- Open source communities +<img src="img/steven-lyons.png" width="300px"> -## Users by role +#### Demographics -- Admin -- Manager -- Developer +- **Age**<br>32 years old +- **Location**<br>Germany +- **Education**<br>Bachelor of Science in Computer Science +- **Occupation**<br>Full-stack web developer +- **Programming experience**<br>Over 10 years +- **Frequently used programming languages**<br>JavaScript, SQL, PHP +- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security. + +#### Motivations +Steven works for a software development company which currently hires around 80 people. When Steven first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Steven felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Steven began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Steven explains why he wanted the engineering team to start using GitLab: + +> +“I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.” +> + +In his role as a full-stack web developer, Steven could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Steven recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab. + +> +“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.” +> + +Undeterred, Steven decided to migrate a couple of projects across to GitLab. + +> +“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.” +> + +Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab. + +The engineering team have been using GitLab CE for around 2 years now. Steven credits himself as being entirely responsible for his company’s decision to move to GitLab. + +#### Frustrations +##### Adoption to GitLab has been slow +Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Steven sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Steven hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits. + +##### Missing Features +Steven’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Steven’s company wants to know if GitLab has a specific feature or does a particular thing, Steven is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Steven gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do. + +##### Regressions and bugs +Steven often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month. + +##### Uses too much RAM and CPU +> +“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHub” +> + +##### UI/UX +GitLab’s interface initially attracted Steven when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.” + +#### Goals +* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration. +* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools. +* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc. + +<hr> + +### James Mackey +- Medium to large size organisations using CE or EE +- Small organisations using EE + +<img src="img/james-mackey.png" width="300px"> + +#### Demographics + +- **Age**<br>36 years old +- **Location**<br>US +- **Education**<br>Masters degree in Computer Science +- **Occupation**<br>Full-stack web developer +- **Programming experience**<br>Over 10 years +- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python +- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics. + +#### Motivations +James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and use”. After using GitLab for over a year, he began to wonder about using it at work. James explains: + +> +“We first installed the CE edition...on a staging server for a PoC and asked a beta team to use it, specifically for the Merge Request features. Soon other teams began asking us to be beta users too, because the team that was already using GitLab was really enjoying it.” +> + +James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHub”. + +The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE. + +James feels partially responsible for his organisation’s decision to start using GitLab. + +> +“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.” +> + +#### Frustrations +##### Third Party Integration +Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do. + +##### UX/UI +James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised. + +##### Permissions +> +“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.” +> + +#### Goals +* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed. +* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. Steven and his team want to be able to understand and use these particular features easily. +* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to Steven. + +<hr> + +### Karolina Plaskaty +- Using GitLab.com for personal/hobby projects +- Would like to use GitLab at work +- Working for a medium to large size organisation + +<img src="img/harry-robison.png" width="300px"> + +#### Demographics + +- **Age**<br>26 years old +- **Location**<br>UK +- **Education**<br>Self taught +- **Occupation**<br>Junior web-developer +- **Programming experience**<br>6 years +- **Frequently used programming languages**<br>JavaScript and SQL +- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel. + +#### Motivations +Harry has been using GitLab.com for around a year. He roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Harry contributes to open source projects to gain programming experience and to give back to the community. He likes GitLab.com for its free private repositories and range of features which provide him with everything he needs for his personal projects. Harry is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. He explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” He’s also an avid reader of GitLab’s blog. + +Harry works for a software development company which currently hires around 500 people. Harry would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. He describes management at his company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Harry is also relatively new to the company so he’s apprehensive about pushing too hard to change version control platforms. + +#### Frustrations +##### Unable to use GitLab at work +Harry wants to use GitLab at work but isn’t sure how to approach the subject with management. In his current role, he doesn’t feel that he has the authority to request GitLab. + +##### Performance +GitLab.com is frequently slow and unavailable. Harry has also heard that GitLab is a “memory hog” which has deterred him from running GitLab on his own machine for just hobby / personal projects. + +##### UX/UI +Harry has an interest in UX and therefore has strong opinions about how GitLab should look and feel. He feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Harry also enjoys contributing to open-source projects, it’s important to him that GitLab is well designed for public repositories, he doesn’t feel that GitLab currently achieves this. + +#### Goals +* To develop his programming experience and to learn from other developers. +* To contribute to both his own and other open source projects. +* To use a fast and intuitive version control platform.
\ No newline at end of file diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 30f0c15dacc..242890af981 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,3 +1 @@ -# GitLab LDAP integration - -This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md). +This document was moved to [`administration/auth/ldap`](../administration/auth/ldap.md). diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index f85f4bf8e1e..5ce99843301 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -90,18 +90,43 @@ inside GitLab that make that possible. It is possible to download the latest artifacts of a job via a well known URL so you can use it for scripting purposes. -The structure of the URL is the following: +The structure of the URL to download the whole artifacts archive is the following: ``` https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name> ``` -For example, to download the latest artifacts of the job named `rspec 6 20` of +To download a single file from the artifacts use the following URL: + +``` +https://example.com/<namespace>/<project>/builds/artifacts/<ref>/file/<path_to_file>?job=<job_name> +``` + +For example, to download the latest artifacts of the job named `coverage` of the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org` namespace, the URL would be: ``` -https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20 +https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=coverage +``` + +To download the file `coverage/index.html` from the same +artifacts use the following URL: + +``` +https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/file/coverage/index.html?job=coverage +``` + +There is also a URL to browse the latest job artifacts: + +``` +https://example.com/<namespace>/<project>/builds/artifacts/<ref>/browse?job=<job_name> +``` + +For example: + +``` +https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/browse?job=coverage ``` The latest builds are also exposed in the UI in various places. Specifically, diff --git a/doc/user/snippets.md b/doc/user/snippets.md new file mode 100644 index 00000000000..417360e08ac --- /dev/null +++ b/doc/user/snippets.md @@ -0,0 +1,19 @@ +# Snippets + +Snippets are little bits of code or text. + +There are 2 types of snippets - project snippets and personal snippets. + +## Project snippets + +Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information. + +## Personal snippets + +Personal snippets are not related to any project and can be created completely independently. There are 3 visibility levels that can be set (public, internal, private - see [Public Access](../public_access/public_access.md) for more information). + +## Downloading snippets + +You can download the raw content of a snippet. + +By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`). diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 7a97b87f1c5..9e7ee47387c 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -39,3 +39,4 @@ - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md) - [Todos](todos.md) +- [Snippets](../user/snippets.md) diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature index d767b088883..ec6666f227f 100644 --- a/features/project/merge_requests/revert.feature +++ b/features/project/merge_requests/revert.feature @@ -5,6 +5,7 @@ Feature: Revert Merge Requests And I am signed in as a developer of the project And I am on the Merge Request detail page And I click on Accept Merge Request + And I am on the Merge Request detail page @javascript Scenario: I revert a merge request diff --git a/lib/api/api.rb b/lib/api/api.rb index dbb7271ccbd..e729c07f8c3 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -19,6 +19,7 @@ module API mount ::API::V3::Repositories mount ::API::V3::SystemHooks mount ::API::V3::Tags + mount ::API::V3::Todos mount ::API::V3::Templates mount ::API::V3::Users end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 173083d0ade..3b314c89c6e 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -16,16 +16,13 @@ module API end params do optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :since, type: String, desc: 'Only commits after or in this date will be returned' - optional :until, type: String, desc: 'Only commits before or in this date will be returned' + optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' + optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' optional :page, type: Integer, default: 0, desc: 'The page for pagination' optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' optional :path, type: String, desc: 'The file path' end get ":id/repository/commits" do - # TODO remove the next line for 9.0, use DateTime type in the params block - datetime_attributes! :since, :until - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' offset = params[:page] * params[:per_page] diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 982645c2f64..69e85c27a65 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -93,20 +93,6 @@ module API end end - desc 'Disable a deploy key for a project' do - detail 'This feature was added in GitLab 8.11' - success Entities::SSHKey - end - params do - requires :key_id, type: Integer, desc: 'The ID of the deploy key' - end - delete ":id/deploy_keys/:key_id/disable" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - key.destroy - - present key.deploy_key, with: Entities::SSHKey - end - desc 'Delete deploy key for a project' do success Key end @@ -115,11 +101,9 @@ module API end delete ":id/deploy_keys/:key_id" do key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) - if key - key.destroy - else - not_found!('Deploy Key') - end + not_found!('Deploy Key') unless key + + key.destroy end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 13896dd91b9..7b6fae866eb 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -160,22 +160,6 @@ module API ActionController::Parameters.new(attrs).permit! end - # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 - # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. - # - # Parameters: - # keys (required) - An array consisting of elements that must be parseable as dates from the params hash - def datetime_attributes!(*keys) - keys.each do |key| - begin - params[key] = Time.xmlschema(params[key]) if params[key].present? - rescue ArgumentError - message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ" - render_api_error!(message, 400) - end - end - end - def filter_by_iid(items, iid) items.where(iid: iid) end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 90fca20d4fa..2b946bfd349 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -10,17 +10,9 @@ module API args.delete(:id) args[:milestone_title] = args.delete(:milestone) + args[:label_name] = args.delete(:labels) - match_all_labels = args.delete(:match_all_labels) - labels = args.delete(:labels) - args[:label_name] = labels if match_all_labels - - issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations - - # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder - if !match_all_labels && labels.present? - issues = issues.includes(:labels).where('labels.title' => labels.split(',')) - end + issues = IssuesFinder.new(current_user, args).execute issues.reorder(args[:order_by] => args[:sort]) end @@ -77,7 +69,7 @@ module API get ":id/issues" do group = find_group!(params[:id]) - issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true) + issues = find_issues(group_id: group.id, state: params[:state] || 'opened') present paginate(issues), with: Entities::Issue, current_user: current_user end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 68c2732ec80..366e5679edd 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -266,7 +266,7 @@ module API desc 'Unstar a project' do success Entities::Project end - delete ':id/star' do + post ':id/unstar' do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reload diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 9bd077263a7..0b9650b296c 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -58,7 +58,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the todo being marked as done' end - delete ':id' do + post ':id/mark_as_done' do todo = current_user.todos.find(params[:id]) TodoService.new.mark_todos_as_done([todo], current_user) @@ -66,9 +66,11 @@ module API end desc 'Mark all todos as done' - delete do + post '/mark_as_done' do todos = find_todos TodoService.new.mark_todos_as_done(todos, current_user) + + no_content! end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 05538f5a42f..fbc17953691 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -314,7 +314,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - put ':id/block' do + post ':id/block' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -330,7 +330,7 @@ module API params do requires :id, type: Integer, desc: 'The ID of the user' end - put ':id/unblock' do + post ':id/unblock' do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb new file mode 100644 index 00000000000..4f9b5fe72a6 --- /dev/null +++ b/lib/api/v3/todos.rb @@ -0,0 +1,28 @@ +module API + module V3 + class Todos < Grape::API + before { authenticate! } + + resource :todos do + desc 'Mark a todo as done' do + success ::API::Entities::Todo + end + params do + requires :id, type: Integer, desc: 'The ID of the todo being marked as done' + end + delete ':id' do + todo = current_user.todos.find(params[:id]) + TodoService.new.mark_todos_as_done([todo], current_user) + + present todo.reload, with: ::API::Entities::Todo, current_user: current_user + end + + desc 'Mark all todos as done' + delete do + todos = TodosFinder.new(current_user, params).execute + TodoService.new.mark_todos_as_done(todos, current_user) + end + end + end + end +end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index ceb139d11b8..e05e457a5df 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -39,6 +39,38 @@ module API present user.emails, with: ::API::Entities::Email end + + desc 'Block a user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + put ':id/block' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + if !user.ldap_blocked? + user.block + else + forbidden!('LDAP blocked users cannot be modified by the API') + end + end + + desc 'Unblock a user. Available only for admins.' + params do + requires :id, type: Integer, desc: 'The ID of the user' + end + put ':id/unblock' do + authenticated_as_admin! + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + if user.ldap_blocked? + forbidden!('LDAP blocked users cannot be unblocked by the API') + else + user.activate + end + end end resource :user do diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 4800a509b37..fc445ab9483 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -54,7 +54,7 @@ module Gitlab disable_statement_timeout - key_name = "fk_#{source}_#{target}_#{column}" + key_name = concurrent_foreign_key_name(source, column) # Using NOT VALID allows us to create a key without immediately # validating it. This means we keep the ALTER TABLE lock only for a @@ -74,6 +74,15 @@ module Gitlab execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") end + # Returns the name for a concurrent foreign key. + # + # PostgreSQL constraint names have a limit of 63 bytes. The logic used + # here is based on Rails' foreign_key_name() method, which unfortunately + # is private so we can't rely on it directly. + def concurrent_foreign_key_name(table, column) + "fk_#{Digest::SHA256.hexdigest("#{table}_#{column}_fk").first(10)}" + end + # Long-running migrations may take more than the timeout allowed by # the database. Disable the session's statement timeout to ensure # migrations don't get killed prematurely. (PostgreSQL only) diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 95dba9a327b..8c80791e7c9 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -1,11 +1,12 @@ module Gitlab module GithubImport class BaseFormatter - attr_reader :formatter, :project, :raw_data + attr_reader :client, :formatter, :project, :raw_data - def initialize(project, raw_data) + def initialize(project, raw_data, client = nil) @project = project @raw_data = raw_data + @client = client @formatter = Gitlab::ImportFormatter.new end @@ -18,19 +19,6 @@ module Gitlab def url raw_data.url || '' end - - private - - def gitlab_user_id(github_id) - User.joins(:identities). - find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s). - try(:id) - end - - def gitlab_author_id - return @gitlab_author_id if defined?(@gitlab_author_id) - @gitlab_author_id = gitlab_user_id(raw_data.user.id) - end end end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index ba869faa92e..7dbeec5b010 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -10,6 +10,7 @@ module Gitlab @access_token = access_token @host = host.to_s.sub(%r{/+\z}, '') @api_version = api_version + @users = {} if access_token ::Octokit.auto_paginate = false @@ -64,6 +65,13 @@ module Gitlab api.respond_to?(method) || super end + def user(login) + return nil unless login.present? + return @users[login] if @users.key?(login) + + @users[login] = api.user(login) + end + private def api_endpoint diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb index 2bddcde2b7c..e21922070c1 100644 --- a/lib/gitlab/github_import/comment_formatter.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -1,6 +1,8 @@ module Gitlab module GithubImport class CommentFormatter < BaseFormatter + attr_writer :author_id + def attributes { project: project, @@ -17,11 +19,11 @@ module Gitlab private def author - raw_data.user.login + @author ||= UserFormatter.new(client, raw_data.user) end def author_id - gitlab_author_id || project.creator_id + author.gitlab_id || project.creator_id end def body @@ -52,10 +54,10 @@ module Gitlab end def note - if gitlab_author_id + if author.gitlab_id body else - formatter.author_line(author) + body + formatter.author_line(author.login) + body end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 9a4ffd28438..d95ff4fd104 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -110,7 +110,7 @@ module Gitlab def import_issues fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| issues.each do |raw| - gh_issue = IssueFormatter.new(project, raw) + gh_issue = IssueFormatter.new(project, raw, client) begin issuable = @@ -131,7 +131,8 @@ module Gitlab def import_pull_requests fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| pull_requests.each do |raw| - gh_pull_request = PullRequestFormatter.new(project, raw) + gh_pull_request = PullRequestFormatter.new(project, raw, client) + next unless gh_pull_request.valid? begin @@ -209,14 +210,16 @@ module Gitlab ActiveRecord::Base.no_touching do comments.each do |raw| begin - comment = CommentFormatter.new(project, raw) + comment = CommentFormatter.new(project, raw, client) + # GH does not return info about comment's parent, so we guess it by checking its URL! *_, parent, iid = URI(raw.html_url).path.split('/') - if parent == 'issues' - issuable = Issue.find_by(project_id: project.id, iid: iid) - else - issuable = MergeRequest.find_by(target_project_id: project.id, iid: iid) - end + + issuable = if parent == 'issues' + Issue.find_by(project_id: project.id, iid: iid) + else + MergeRequest.find_by(target_project_id: project.id, iid: iid) + end next unless issuable diff --git a/lib/gitlab/github_import/issuable_formatter.rb b/lib/gitlab/github_import/issuable_formatter.rb index 256f360efc7..29fb0f9d333 100644 --- a/lib/gitlab/github_import/issuable_formatter.rb +++ b/lib/gitlab/github_import/issuable_formatter.rb @@ -1,6 +1,8 @@ module Gitlab module GithubImport class IssuableFormatter < BaseFormatter + attr_writer :assignee_id, :author_id + def project_association raise NotImplementedError end @@ -23,18 +25,24 @@ module Gitlab raw_data.assignee.present? end - def assignee_id + def author + @author ||= UserFormatter.new(client, raw_data.user) + end + + def author_id + @author_id ||= author.gitlab_id || project.creator_id + end + + def assignee if assigned? - gitlab_user_id(raw_data.assignee.id) + @assignee ||= UserFormatter.new(client, raw_data.assignee) end end - def author - raw_data.user.login - end + def assignee_id + return @assignee_id if defined?(@assignee_id) - def author_id - gitlab_author_id || project.creator_id + @assignee_id = assignee.try(:gitlab_id) end def body @@ -42,10 +50,10 @@ module Gitlab end def description - if gitlab_author_id + if author.gitlab_id body else - formatter.author_line(author) + body + formatter.author_line(author.login) + body end end diff --git a/lib/gitlab/github_import/user_formatter.rb b/lib/gitlab/github_import/user_formatter.rb new file mode 100644 index 00000000000..04c2964da20 --- /dev/null +++ b/lib/gitlab/github_import/user_formatter.rb @@ -0,0 +1,45 @@ +module Gitlab + module GithubImport + class UserFormatter + attr_reader :client, :raw + + delegate :id, :login, to: :raw, allow_nil: true + + def initialize(client, raw) + @client = client + @raw = raw + end + + def gitlab_id + return @gitlab_id if defined?(@gitlab_id) + + @gitlab_id = find_by_external_uid || find_by_email + end + + private + + def email + @email ||= client.user(raw.login).try(:email) + end + + def find_by_email + return nil unless email + + User.find_by_any_email(email) + .try(:id) + end + + def find_by_external_uid + return nil unless id + + identities = ::Identity.arel_table + + User.select(:id) + .joins(:identities).where(identities[:provider].eq(:github) + .and(identities[:extern_uid].eq(id))) + .first + .try(:id) + end + end + end +end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index b36d0e69330..7d4636e98d1 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -86,32 +86,47 @@ describe Projects::BlobController do end context 'when user has forked project' do - let(:guest) { create(:user) } - let!(:forked_project) { Projects::ForkService.new(project, guest).execute } - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") } - - before { sign_in(guest) } - - it "redirects to forked project new merge request" do - default_params[:target_branch] = "fork-test-1" - default_params[:create_merge_request] = 1 - - allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success) - - put :update, default_params - - expect(response).to redirect_to( - new_namespace_project_merge_request_path( - forked_project.namespace, - forked_project, - merge_request: { - source_project_id: forked_project.id, - target_project_id: project.id, - source_branch: "fork-test-1", - target_branch: "master" - } + let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } + let!(:forked_project) { forked_project_link.forked_to_project } + let(:guest) { forked_project.owner } + + before do + sign_in(guest) + end + + context 'when editing on the fork' do + before do + default_params[:namespace_id] = forked_project.namespace.to_param + default_params[:project_id] = forked_project.to_param + end + + it 'redirects to blob' do + put :update, default_params + + expect(response).to redirect_to(namespace_project_blob_path(forked_project.namespace, forked_project, 'master/CHANGELOG')) + end + end + + context 'when editing on the original repository' do + it "redirects to forked project new merge request" do + default_params[:target_branch] = "fork-test-1" + default_params[:create_merge_request] = 1 + + put :update, default_params + + expect(response).to redirect_to( + new_namespace_project_merge_request_path( + forked_project.namespace, + forked_project, + merge_request: { + source_project_id: forked_project.id, + target_project_id: project.id, + source_branch: "fork-test-1", + target_branch: "master" + } + ) ) - ) + end end end end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 19e948d8fb8..77ee10a1e15 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -206,4 +206,37 @@ describe Projects::SnippetsController do end end end + + describe 'GET #raw' do + let(:project_snippet) do + create( + :project_snippet, :public, + project: project, + author: user, + content: "first line\r\nsecond line\r\nthird line" + ) + end + + context 'CRLF line ending' do + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: project_snippet.to_param + } + end + + it 'returns LF line endings by default' do + get :raw, params + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get :raw, params.merge(line_ending: :raw) + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end + end end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index dadcb90cfc2..f90c0d76ceb 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -286,6 +286,24 @@ describe SnippetsController do expect(assigns(:snippet)).to eq(personal_snippet) expect(response).to have_http_status(200) end + + context 'CRLF line ending' do + let(:personal_snippet) do + create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") + end + + it 'returns LF line endings by default' do + get action, id: personal_snippet.to_param + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get action, id: personal_snippet.to_param, line_ending: :raw + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end end context 'when not signed in' do diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb index 109de39b2dd..14c193f7450 100644 --- a/spec/features/groups/members/list_spec.rb +++ b/spec/features/groups/members/list_spec.rb @@ -30,6 +30,21 @@ feature 'Groups members list', feature: true do expect(second_row).to be_blank end + it 'updates user to owner level', :js do + group.add_owner(user1) + group.add_developer(user2) + + visit group_group_members_path(group) + + page.within(second_row) do + click_button('Developer') + + click_link('Owner') + + expect(page).to have_button('Owner') + end + end + def first_row page.all('ul.content-list > li')[0] end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index b56e562b2b6..45185f2dd1f 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -19,6 +19,51 @@ feature "New project", feature: true do end end + context "Namespace selector" do + context "with user namespace" do + before do + visit new_project_path + end + + it "selects the user namespace" do + namespace = find("#project_namespace_id") + + expect(namespace.text).to eq user.username + end + end + + context "with group namespace" do + let(:group) { create(:group, :private, owner: user) } + + before do + group.add_owner(user) + visit new_project_path(namespace_id: group.id) + end + + it "selects the group namespace" do + namespace = find("#project_namespace_id option[selected]") + + expect(namespace.text).to eq group.name + end + + context "on validation error" do + before do + fill_in('project_path', with: 'private-group-project') + choose('Internal') + click_button('Create project') + + expect(page).to have_css '.project-edit-errors .alert.alert-danger' + end + + it "selects the group namespace" do + namespace = find("#project_namespace_id option[selected]") + + expect(namespace.text).to eq group.name + end + end + end + end + context 'Import project options' do before do visit new_project_path diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 4eafac1acd8..3b8f0b2d3f8 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -20,6 +20,8 @@ feature 'Ref switcher', feature: true, js: true do input.set 'binary' wait_for_ajax + expect(find('.dropdown-content ul')).to have_selector('li', count: 6) + page.within '.dropdown-content ul' do input.native.send_keys :enter end diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index 84c0e9cbfe2..a91801cfc89 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -99,6 +99,29 @@ require('~/filtered_search/filtered_search_tokenizer'); expect(results.tokens[2].value).toBe('Doing'); expect(results.tokens[2].symbol).toBe('~'); }); + + it('returns search value for invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + expect(results.tokens.length).toEqual(0); + }); + + it('returns search value and token for mix of valid and invalid tokens', () => { + const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); + expect(results.tokens.length).toEqual(1); + expect(results.tokens[0].key).toBe('label'); + expect(results.tokens[0].value).toBe('real'); + expect(results.tokens[0].symbol).toBe(''); + expect(results.lastToken).toBe('fake:token'); + expect(results.searchToken).toBe('fake:token'); + }); + + it('returns search value for invalid symbols', () => { + const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); + expect(results.lastToken).toBe('std::includes'); + expect(results.searchToken).toBe('std::includes'); + }); }); }); })(); diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index e94ca4fcfd2..e007044868c 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -101,6 +101,16 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end + describe '#concurrent_foreign_key_name' do + it 'returns the name for a foreign key' do + name = model.concurrent_foreign_key_name(:this_is_a_very_long_table_name, + :with_a_very_long_column_name) + + expect(name).to be_an_instance_of(String) + expect(name.length).to eq(13) + end + end + describe '#disable_statement_timeout' do context 'using PostgreSQL' do it 'disables statement timeouts' do diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb index e6e33d3686a..cc38872e426 100644 --- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Gitlab::GithubImport::CommentFormatter, lib: true do + let(:client) { double } let(:project) { create(:empty_project) } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } let(:base) do @@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do } end - subject(:comment) { described_class.new(project, raw)} + subject(:comment) { described_class.new(project, raw, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end describe '#attributes' do context 'when do not reference a portion of the diff' do @@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do context 'when author is a GitLab user' do let(:raw) { double(base.merge(user: octocat)) } - it 'returns GitLab user id as author_id' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id + end + + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index afd78abdc9b..33d83d6d2f1 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -44,6 +44,7 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound) allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error) + allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat) allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2]) allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone]) allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) @@ -53,7 +54,8 @@ describe Gitlab::GithubImport::Importer, lib: true do allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil })) allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) end - let(:octocat) { double(id: 123456, login: 'octocat') } + + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:label1) do @@ -125,6 +127,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ) end + let!(:user) { create(:user, email: octocat.email) } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb index eec1fabab54..f34d09f2c1d 100644 --- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Gitlab::GithubImport::IssueFormatter, lib: true do + let(:client) { double } let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do } end - subject(:issue) { described_class.new(project, raw_data) } + subject(:issue) { described_class.new(project, raw_data, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do context 'when issue is open' do @@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do expect(issue.attributes.fetch(:assignee_id)).to be_nil end - it 'returns GitLab user id as assignee_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as assignee_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id end + + it 'returns GitLab user id associated with GitHub email as assignee_id' do + gl_user = create(:user, email: octocat.email) + + expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + end end context 'when it has a milestone' do @@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do context 'when author is a GitLab user' do let(:raw_data) { double(base_data.merge(user: octocat)) } - it 'returns project#creator_id as author_id when is not a GitLab user' do + it 'returns project creator_id as author_id when is not a GitLab user' do expect(issue.attributes.fetch(:author_id)).to eq project.creator_id end - it 'returns GitLab user id as author_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(issue.attributes.fetch(:author_id)).to eq gl_user.id end + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + + expect(issue.attributes.fetch(:author_id)).to eq gl_user.id + end + it 'returns description without created at tag line' do create(:omniauth_user, extern_uid: octocat.id, provider: 'github') diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 90947ff4707..e46be18aa99 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::GithubImport::PullRequestFormatter, lib: true do + let(:client) { double } let(:project) { create(:project, :repository) } let(:source_sha) { create(:commit, project: project).id } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } @@ -10,7 +11,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:target_repo) { repository } let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } - let(:octocat) { double(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:base_data) do @@ -32,7 +33,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do } end - subject(:pull_request) { described_class.new(project, raw_data) } + subject(:pull_request) { described_class.new(project, raw_data, client) } + + before do + allow(client).to receive(:user).and_return(octocat) + end shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do context 'when pull request is open' do @@ -121,26 +126,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do expect(pull_request.attributes.fetch(:assignee_id)).to be_nil end - it 'returns GitLab user id as assignee_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as assignee_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id end + + it 'returns GitLab user id associated with GitHub email as assignee_id' do + gl_user = create(:user, email: octocat.email) + + expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id + end end context 'when author is a GitLab user' do let(:raw_data) { double(base_data.merge(user: octocat)) } - it 'returns project#creator_id as author_id when is not a GitLab user' do + it 'returns project creator_id as author_id when is not a GitLab user' do expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id end - it 'returns GitLab user id as author_id when is a GitLab user' do + it 'returns GitLab user id associated with GitHub id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id end + it 'returns GitLab user id associated with GitHub email as author_id' do + gl_user = create(:user, email: octocat.email) + + expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id + end + it 'returns description without created at tag line' do create(:omniauth_user, extern_uid: octocat.id, provider: 'github') diff --git a/spec/lib/gitlab/github_import/user_formatter_spec.rb b/spec/lib/gitlab/github_import/user_formatter_spec.rb new file mode 100644 index 00000000000..db792233657 --- /dev/null +++ b/spec/lib/gitlab/github_import/user_formatter_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::UserFormatter, lib: true do + let(:client) { double } + let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } + + subject(:user) { described_class.new(client, octocat) } + + before do + allow(client).to receive(:user).and_return(octocat) + end + + describe '#gitlab_id' do + context 'when GitHub user is a GitLab user' do + it 'return GitLab user id when user associated their account with GitHub' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(user.gitlab_id).to eq gl_user.id + end + + it 'returns GitLab user id when user primary email matches GitHub email' do + gl_user = create(:user, email: octocat.email) + + expect(user.gitlab_id).to eq gl_user.id + end + + it 'returns GitLab user id when any of user linked emails matches GitHub email' do + gl_user = create(:user, email: 'johndoe@example.com') + create(:email, user: gl_user, email: octocat.email) + + expect(user.gitlab_id).to eq gl_user.id + end + end + + it 'returns nil when GitHub user is not a GitLab user' do + expect(user.gitlab_id).to be_nil + end + end +end diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb index 32935bc0b09..b6e5c95d18a 100644 --- a/spec/models/concerns/spammable_spec.rb +++ b/spec/models/concerns/spammable_spec.rb @@ -14,8 +14,9 @@ describe Issue, 'Spammable' do end describe 'InstanceMethods' do + let(:issue) { build(:issue, spam: true) } + it 'should be invalid if spam' do - issue = build(:issue, spam: true) expect(issue.valid?).to be_falsey end @@ -29,5 +30,20 @@ describe Issue, 'Spammable' do expect(issue.check_for_spam?).to eq(false) end end + + describe '#submittable_as_spam_by?' do + let(:admin) { build(:admin) } + let(:user) { build(:user) } + + before do + allow(issue).to receive(:submittable_as_spam?).and_return(true) + end + + it 'tests if the user can submit spam' do + expect(issue.submittable_as_spam_by?(admin)).to be(true) + expect(issue.submittable_as_spam_by?(user)).to be(false) + expect(issue.submittable_as_spam_by?(nil)).to be_nil + end + end end end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 5cba546bee2..81a8856b8f1 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -21,10 +21,6 @@ describe API::CommitStatuses, api: true do let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } - it_behaves_like 'a paginated resources' do - let(:request) { get api(get_url, reporter) } - end - context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } @@ -45,6 +41,7 @@ describe API::CommitStatuses, api: true do it 'returns latest commit statuses' do expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id) json_response.sort_by!{ |status| status['id'] } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 9fa007332f0..ecc6a597869 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -72,7 +72,7 @@ describe API::Commits, api: true do get api("/projects/#{project.id}/repository/commits?since=invalid-date", user) expect(response).to have_http_status(400) - expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format" + expect(json_response['error']).to eq('since is invalid') end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 67039bb037e..7e682e91bd1 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -148,25 +148,4 @@ describe API::DeployKeys, api: true do end end end - - describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do - context 'when the user can admin the project' do - it 'disables the key' do - expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin) - end.to change { project.deploy_keys.count }.from(1).to(0) - - expect(response).to have_http_status(200) - expect(json_response['id']).to eq(deploy_key.id) - end - end - - context 'when authenticated as non-admin user' do - it 'should return a 404 error' do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user) - - expect(response).to have_http_status(404) - end - end - end end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 5c4ce39f70c..e55575ffbda 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -14,10 +14,6 @@ describe API::Deployments, api: true do describe 'GET /projects/:id/deployments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/deployments", user) } - end - it 'returns projects deployments' do get api("/projects/#{project.id}/deployments", user) diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index b0ee196666e..d0958d39d44 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -14,10 +14,6 @@ describe API::Environments, api: true do describe 'GET /projects/:id/environments' do context 'as member of the project' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/environments", user) } - end - it 'returns project environments' do get api("/projects/#{project.id}/environments", user) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 74ac7955cb8..ece1b43567d 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -117,14 +117,20 @@ describe API::Issues, api: true do expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled issues when at least one label matches' do - get api("/issues?labels=#{label.title},foo,bar", user) + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: project) + label_c = create(:label, title: 'bar', project: project) + + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api("/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}" expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) end it 'returns an empty array if no issue matches labels' do @@ -356,6 +362,21 @@ describe API::Issues, api: true do expect(json_response.length).to eq(0) end + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: group_project) + label_c = create(:label, title: 'bar', project: group_project) + + create(:label_link, label: label_b, target: group_issue) + create(:label_link, label: label_c, target: group_issue) + + get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) + end + it 'returns an empty array if no group issue matches labels' do get api("#{base_url}?labels=foo,bar", user) @@ -549,14 +570,28 @@ describe API::Issues, api: true do expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled project issues where all labels match' do - get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: project) + label_c = create(:label, title: 'bar', project: project) + + create(:label_link, label: label_b, target: issue) + create(:label_link, label: label_c, target: issue) + + get api("#{base_url}/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}" expect(response).to have_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) - expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title]) + end + + it 'returns an empty array if not all labels matches' do + get api("#{base_url}/issues?labels=#{label.title},foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'returns an empty array if no project issue matches labels' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 0d3040519bc..3cca4468be7 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -32,10 +32,6 @@ describe API::Notes, api: true do before { project.team << [user, :reporter] } describe "GET /projects/:id/noteable/:noteable_id/notes" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) } - end - context "when noteable is an Issue" do it "returns an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index b7a0b5a9e13..98d004b572e 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -15,15 +15,12 @@ describe API::Pipelines, api: true do before { project.team << [user, :master] } describe 'GET /projects/:id/pipelines ' do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/projects/#{project.id}/pipelines", user) } - end - context 'authorized user' do it 'returns project pipelines' do get api("/projects/#{project.id}/pipelines", user) expect(response).to have_http_status(200) + expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['sha']).to match /\A\h{40}\z/ expect(json_response.first['id']).to eq pipeline.id diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index db70b63917e..4e90aae9279 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1235,7 +1235,7 @@ describe API::Projects, api: true do end end - describe 'DELETE /projects/:id/star' do + describe 'POST /projects/:id/unstar' do context 'on a starred project' do before do user.toggle_star(project) @@ -1243,16 +1243,16 @@ describe API::Projects, api: true do end it 'unstars the project' do - expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1) - expect(response).to have_http_status(200) + expect(response).to have_http_status(201) expect(json_response['star_count']).to eq(0) end end context 'on an unstarred project' do it 'does not modify the star count' do - expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count } expect(response).to have_http_status(304) end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 2069d2a7c75..f35e963a14b 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -107,46 +107,47 @@ describe API::Todos, api: true do end end - describe 'DELETE /todos/:id' do + describe 'POST /todos/:id/mark_as_done' do context 'when unauthenticated' do it 'returns authentication error' do - delete api("/todos/#{pending_1.id}") + post api("/todos/#{pending_1.id}/mark_as_done") - expect(response.status).to eq(401) + expect(response).to have_http_status(401) end end context 'when authenticated' do it 'marks a todo as done' do - delete api("/todos/#{pending_1.id}", john_doe) + post api("/todos/#{pending_1.id}/mark_as_done", john_doe) - expect(response.status).to eq(200) + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(pending_1.id) + expect(json_response['state']).to eq('done') expect(pending_1.reload).to be_done end it 'updates todos cache' do expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - delete api("/todos/#{pending_1.id}", john_doe) + post api("/todos/#{pending_1.id}/mark_as_done", john_doe) end end end - describe 'DELETE /todos' do + describe 'POST /mark_as_done' do context 'when unauthenticated' do it 'returns authentication error' do - delete api('/todos') + post api('/todos/mark_as_done') - expect(response.status).to eq(401) + expect(response).to have_http_status(401) end end context 'when authenticated' do it 'marks all todos as done' do - delete api('/todos', john_doe) + post api('/todos/mark_as_done', john_doe) - expect(response.status).to eq(200) - expect(response.body).to eq('3') + expect(response).to have_http_status(204) expect(pending_1.reload).to be_done expect(pending_2.reload).to be_done expect(pending_3.reload).to be_done @@ -155,7 +156,7 @@ describe API::Todos, api: true do it 'updates todos cache' do expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original - delete api("/todos", john_doe) + post api("/todos/mark_as_done", john_doe) end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 7ece22f1934..603da9f49fc 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1003,69 +1003,69 @@ describe API::Users, api: true do end end - describe 'PUT /users/:id/block' do + describe 'POST /users/:id/block' do before { admin } it 'blocks existing user' do - put api("/users/#{user.id}/block", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('blocked') end it 'does not re-block ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/block", admin) + post api("/users/#{ldap_blocked_user.id}/block", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/block", user) + post api("/users/#{user.id}/block", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end end - describe 'PUT /users/:id/unblock' do + describe 'POST /users/:id/unblock' do let(:blocked_user) { create(:user, state: 'blocked') } before { admin } it 'unblocks existing user' do - put api("/users/#{user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(user.reload.state).to eq('active') end it 'unblocks a blocked user' do - put api("/users/#{blocked_user.id}/unblock", admin) - expect(response).to have_http_status(200) + post api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(201) expect(blocked_user.reload.state).to eq('active') end it 'does not unblock ldap blocked users' do - put api("/users/#{ldap_blocked_user.id}/unblock", admin) + post api("/users/#{ldap_blocked_user.id}/unblock", admin) expect(response).to have_http_status(403) expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'does not be available for non admin users' do - put api("/users/#{user.id}/unblock", user) + post api("/users/#{user.id}/unblock", user) expect(response).to have_http_status(403) expect(user.reload.state).to eq('active') end it 'returns a 404 error if user id not found' do - put api('/users/9999/block', admin) + post api('/users/9999/block', admin) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 for invalid ID" do - put api("/users/ASDF/block", admin) + post api("/users/ASDF/block", admin) expect(response).to have_http_status(404) end @@ -1093,14 +1093,14 @@ describe API::Users, api: true do end context "as a user than can see the event's project" do - it_behaves_like 'a paginated resources' do - let(:request) { get api("/users/#{user.id}/events", user) } - end - context 'joined event' do it 'returns the "joined" event' do get api("/users/#{user.id}/events", user) + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + comment_event = json_response.find { |e| e['action_name'] == 'commented on' } expect(comment_event['project_id'].to_i).to eq(project.id) diff --git a/spec/requests/api/v3/todos_spec.rb b/spec/requests/api/v3/todos_spec.rb new file mode 100644 index 00000000000..80fa697e949 --- /dev/null +++ b/spec/requests/api/v3/todos_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe API::V3::Todos, api: true do + include ApiHelpers + + let(:project_1) { create(:empty_project) } + let(:project_2) { create(:empty_project) } + let(:author_1) { create(:user) } + let(:author_2) { create(:user) } + let(:john_doe) { create(:user, username: 'john_doe') } + let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) } + let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) } + let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) } + let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) } + + before do + project_1.team << [john_doe, :developer] + project_2.team << [john_doe, :developer] + end + + describe 'DELETE /todos/:id' do + context 'when unauthenticated' do + it 'returns authentication error' do + delete v3_api("/todos/#{pending_1.id}") + + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'marks a todo as done' do + delete v3_api("/todos/#{pending_1.id}", john_doe) + + expect(response.status).to eq(200) + expect(pending_1.reload).to be_done + end + + it 'updates todos cache' do + expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original + + delete v3_api("/todos/#{pending_1.id}", john_doe) + end + end + end + + describe 'DELETE /todos' do + context 'when unauthenticated' do + it 'returns authentication error' do + delete v3_api('/todos') + + expect(response.status).to eq(401) + end + end + + context 'when authenticated' do + it 'marks all todos as done' do + delete v3_api('/todos', john_doe) + + expect(response.status).to eq(200) + expect(response.body).to eq('3') + expect(pending_1.reload).to be_done + expect(pending_2.reload).to be_done + expect(pending_3.reload).to be_done + end + + it 'updates todos cache' do + expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original + + delete v3_api("/todos", john_doe) + end + end + end +end diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index 7022f87bc51..5020ef18a3a 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -7,6 +7,7 @@ describe API::V3::Users, api: true do let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } let(:email) { create(:email, user: user) } + let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } describe 'GET /user/:id/keys' do before { admin } @@ -117,4 +118,72 @@ describe API::V3::Users, api: true do end end end + + describe 'PUT /users/:id/block' do + before { admin } + it 'blocks existing user' do + put v3_api("/users/#{user.id}/block", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('blocked') + end + + it 'does not re-block ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/block", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/block", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + end + + describe 'PUT /users/:id/unblock' do + let(:blocked_user) { create(:user, state: 'blocked') } + before { admin } + + it 'unblocks existing user' do + put v3_api("/users/#{user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(user.reload.state).to eq('active') + end + + it 'unblocks a blocked user' do + put v3_api("/users/#{blocked_user.id}/unblock", admin) + expect(response).to have_http_status(200) + expect(blocked_user.reload.state).to eq('active') + end + + it 'does not unblock ldap blocked users' do + put v3_api("/users/#{ldap_blocked_user.id}/unblock", admin) + expect(response).to have_http_status(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + + it 'does not be available for non admin users' do + put v3_api("/users/#{user.id}/unblock", user) + expect(response).to have_http_status(403) + expect(user.reload.state).to eq('active') + end + + it 'returns a 404 error if user id not found' do + put v3_api('/users/9999/block', admin) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it "returns a 404 for invalid ID" do + put v3_api("/users/ASDF/block", admin) + + expect(response).to have_http_status(404) + end + end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 3faa88c00a1..74bfba44dfd 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -50,6 +50,25 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end + context 'when flushing caches fail' do + before do + new_user = create(:user) + project.team.add_user(new_user, Gitlab::Access::DEVELOPER) + allow_any_instance_of(Projects::DestroyService).to receive(:flush_caches).and_raise(Redis::CannotConnectError) + end + + it 'keeps project team intact upon an error' do + Sidekiq::Testing.inline! do + begin + destroy_project(project, user, {}) + rescue Redis::CannotConnectError + end + end + + expect(project.team.members.count).to eq 1 + end + end + context 'with async_execute' do let(:async) { true } diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb index 46e58393218..c0bf27c698c 100644 --- a/spec/services/users/destroy_spec.rb +++ b/spec/services/users/destroy_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' describe Users::DestroyService, services: true do describe "Deletes a user and all their personal projects" do - let!(:user) { create(:user) } - let!(:current_user) { create(:user) } - let!(:namespace) { create(:namespace, owner: user) } - let!(:project) { create(:project, namespace: namespace) } - let(:service) { described_class.new(current_user) } + let!(:user) { create(:user) } + let!(:admin) { create(:admin) } + let!(:namespace) { create(:namespace, owner: user) } + let!(:project) { create(:project, namespace: namespace) } + let(:service) { described_class.new(admin) } context 'no options are given' do it 'deletes the user' do @@ -57,5 +57,26 @@ describe Users::DestroyService, services: true do expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) end end + + context "deletion permission checks" do + it 'does not delete the user when user is not an admin' do + other_user = create(:user) + + expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect(User.exists?(user.id)).to be(true) + end + + it 'allows admins to delete anyone' do + described_class.new(admin).execute(user) + + expect(User.exists?(user.id)).to be(false) + end + + it 'allows users to delete their own account' do + described_class.new(user).execute(user) + + expect(User.exists?(user.id)).to be(false) + end + end end end diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb deleted file mode 100644 index 352a6eeec79..00000000000 --- a/spec/support/api/pagination_shared_examples.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Specs for paginated resources. -# -# Requires an API request: -# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } -shared_examples 'a paginated resources' do - before do - # Fires the request - request - end - - it 'has pagination headers' do - expect(response.headers).to include('X-Total') - expect(response.headers).to include('X-Total-Pages') - expect(response.headers).to include('X-Per-Page') - expect(response.headers).to include('X-Page') - expect(response.headers).to include('X-Next-Page') - expect(response.headers).to include('X-Prev-Page') - expect(response.headers).to include('Link') - end -end |