diff options
67 files changed, 760 insertions, 329 deletions
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index b37698fe9ca..3f083655f95 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -11,7 +11,6 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; - this.cantEdit = cantEdit; } diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 2af242a69df..5838b1bdbb7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -56,7 +56,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { } renderContent() { - const dropdownData = gl.FilteredSearchTokenKeys.get() + const dropdownData = this.tokenKeys.get() .map(tokenKey => ({ icon: `fa-${tokenKey.icon}`, hint: tokenKey.key, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 8155218681c..76cb71b6c12 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -1,5 +1,5 @@ -import statusCodes from '~/lib/utils/http_status'; -import { bytesToMiB } from '~/lib/utils/number_utils'; +import statusCodes from '../../lib/utils/http_status'; +import { bytesToMiB } from '../../lib/utils/number_utils'; import MemoryGraph from '../../vue_shared/components/memory_graph'; import MRWidgetService from '../services/mr_widget_service'; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index cfbaaaa04c7..880ab52fa1b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -152,7 +152,7 @@ } .value-container { - background-color: $filter-value-selected-color; + box-shadow: inset 0 0 0 100px $filtered-search-term-shadow-color; } } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index a78179e727f..61e3897f369 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -125,10 +125,11 @@ label { .select-wrapper { position: relative; - .fa-caret-down { + .fa-chevron-down { position: absolute; + font-size: 10px; right: 10px; - top: 10px; + top: 12px; color: $gray-darkest; pointer-events: none; } @@ -138,6 +139,12 @@ label { padding-left: 10px; padding-right: 10px; -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + &::-ms-expand { + display: none; + } } .form-control-inline { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 1b20c35ad98..40e654f4838 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -18,19 +18,28 @@ background-image: none; background-color: transparent; border: none; - padding-top: 6px; - padding-right: 10px; + padding-top: 12px; + padding-right: 20px; + font-size: 10px; b { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 5px dashed; - border-right: 5px solid transparent; - border-left: 5px solid transparent; + display: none; + } + + &::after { + content: "\f078"; + position: absolute; + z-index: 1; + text-align: center; + pointer-events: none; + box-sizing: border-box; color: $gray-darkest; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 49ba0108228..71513199d84 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -282,6 +282,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* * Filtered Search */ +$filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09); $dropdown-hover-color: $blue-400; /* diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 928f17e6a8e..7d0e2b3e2ef 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController include ActionController::HttpAuthentication::Basic include KerberosSpnegoHelper - attr_reader :authentication_result + attr_reader :authentication_result, :redirected_path delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true @@ -14,7 +14,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController skip_before_action :verify_authenticity_token skip_before_action :repository before_action :authenticate_user - before_action :ensure_project_found! private @@ -68,38 +67,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? end - def ensure_project_found! - render_not_found if project.blank? - end - def project - return @project if defined?(@project) - - project_id, _ = project_id_with_suffix - @project = - if project_id.blank? - nil - else - Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") - end - end + parse_repo_path unless defined?(@project) - # This method returns two values so that we can parse - # params[:project_id] (untrusted input!) in exactly one place. - def project_id_with_suffix - id = params[:project_id] || '' - - %w[.wiki.git .git].each do |suffix| - if id.end_with?(suffix) - # Be careful to only remove the suffix from the end of 'id'. - # Accidentally removing it from the middle is how security - # vulnerabilities happen! - return [id.slice(0, id.length - suffix.length), suffix] - end - end + @project + end - # Something is wrong with params[:project_id]; do not pass it on. - [nil, nil] + def parse_repo_path + @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}") end def render_missing_personal_token @@ -114,14 +89,9 @@ class Projects::GitHttpClientController < Projects::ApplicationController end def wiki? - return @wiki if defined?(@wiki) - - _, suffix = project_id_with_suffix - @wiki = suffix == '.wiki.git' - end + parse_repo_path unless defined?(@wiki) - def render_not_found - render plain: 'Not Found', status: :not_found + @wiki end def handle_basic_authentication(login, password) diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index b6b62da7b60..71ae60cb8cd 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -56,7 +56,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def access - @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities) + @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path) end def access_actor diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 957ad875858..558f8b5e2e5 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -41,6 +41,7 @@ class IssuableFinder items = by_iids(items) items = by_milestone(items) items = by_label(items) + items = by_created_at(items) # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far items = by_project(items) @@ -402,6 +403,18 @@ class IssuableFinder params[:non_archived].present? ? items.non_archived : items end + def by_created_at(items) + if params[:created_after].present? + items = items.where(items.klass.arel_table[:created_at].gteq(params[:created_after])) + end + + if params[:created_before].present? + items = items.where(items.klass.arel_table[:created_at].lteq(params[:created_before])) + end + + items + end + def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 84c09a32987..0d0459f5a70 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -151,14 +151,21 @@ module ProjectsHelper disabled: disabled_option ) - content_tag( - :select, - options, - name: "project[project_feature_attributes][#{field}]", - id: "project_project_feature_attributes_#{field}", - class: "pull-right form-control #{repo_children_classes(field)}", - data: { field: field } - ).html_safe + content_tag :div, class: "select-wrapper" do + concat( + content_tag( + :select, + options, + name: "project[project_feature_attributes][#{field}]", + id: "project_project_feature_attributes_#{field}", + class: "pull-right form-control select-control #{repo_children_classes(field)} ", + data: { field: field } + ) + ) + concat( + icon('chevron-down') + ) + end.html_safe end def link_to_autodeploy_doc diff --git a/app/models/user.rb b/app/models/user.rb index 5d128e4b390..782c162e1f3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -139,21 +139,21 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validate :namespace_uniq, if: ->(user) { user.username_changed? } + validate :namespace_uniq, if: :username_changed? validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } - validate :unique_email, if: ->(user) { user.email_changed? } - validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } - validate :owns_public_email, if: ->(user) { user.public_email_changed? } + validate :unique_email, if: :email_changed? + validate :owns_notification_email, if: :notification_email_changed? + validate :owns_public_email, if: :public_email_changed? validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } before_validation :sanitize_attrs - before_validation :set_notification_email, if: ->(user) { user.email_changed? } - before_validation :set_public_email, if: ->(user) { user.public_email_changed? } + before_validation :set_notification_email, if: :email_changed? + before_validation :set_public_email, if: :public_email_changed? - after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } + after_update :update_emails_with_primary_email, if: :email_changed? before_save :ensure_authentication_token, :ensure_incoming_email_token - before_save :ensure_external_user_rights + before_save :ensure_user_rights_and_limits, if: :external_changed? after_save :ensure_namespace_correct after_initialize :set_projects_limit after_destroy :post_destroy_hook @@ -1033,11 +1033,14 @@ class User < ActiveRecord::Base super end - def ensure_external_user_rights - return unless external? - - self.can_create_group = false - self.projects_limit = 0 + def ensure_user_rights_and_limits + if external? + self.can_create_group = false + self.projects_limit = 0 + else + self.can_create_group = gitlab_config.default_can_create_group + self.projects_limit = current_application_settings.default_projects_limit + end end def signup_domain_valid? diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 3e07b811027..f028f5eb0d4 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -34,7 +34,7 @@ module Users # Keep trying until we obtain the lease. If we don't do so we may end up # not updating the list of authorized projects properly. To prevent # hammering Redis too much we'll wait for a bit between retries. - sleep(1) + sleep(0.1) end begin diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 2269fb1fd8c..5a4ed1c3a2a 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -21,11 +21,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.text_field :color, class: "form-control" + = f.color_field :color, class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.text_field :font, class: "form-control" + = f.color_field :font, class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index fcfd350f0da..15672289c65 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -42,10 +42,17 @@ - if current_user.ldap_user? Some options are unavailable for LDAP accounts .col-lg-9 - .form-group - = f.label :name, class: "label-light" - = f.text_field :name, class: "form-control", required: true - %span.help-block Enter your name, so people you know can recognize you. + .row + .form-group.col-md-9 + = f.label :name, class: "label-light" + = f.text_field :name, class: "form-control", required: true + %span.help-block Enter your name, so people you know can recognize you. + + .form-group.col-md-3 + = f.label :id, class: 'label-light' do + User ID + = f.text_field :id, class: 'form-control', readonly: true + .form-group = f.label :email, class: "label-light" diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml index 65fc0a36ca9..4026b9e3c46 100644 --- a/app/views/projects/_visibility_select.html.haml +++ b/app/views/projects/_visibility_select.html.haml @@ -1,5 +1,7 @@ - if can_change_visibility_level?(@project, current_user) - = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select') + .select-wrapper + = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select select-control') + = icon('chevron-down') - else .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } } = visibility_level_icon(@project.visibility_level) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index df97b4d9c13..7fe44816bae 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -6,7 +6,7 @@ = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard")) %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} - %span= _('ByAuthor|by') + %span= s_('ByAuthor|by') = author_avatar(@commit, size: 24) %strong = commit_author_link(@commit, avatar: true, size: 24) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index c3dab68cea5..296e37e20e6 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -99,9 +99,9 @@ Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' } - - + .select-wrapper + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' } + = icon('chevron-down') - if Gitlab.config.registry.enabled .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) } .checkbox diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 247c4bdbe2d..8bf2246662a 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -6,7 +6,9 @@ = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group = label_tag :access_level, "Choose a role permission", class: "label-light" - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .select-wrapper + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" + = icon('chevron-down') .help-block.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml index b7cc8dd7062..643569db646 100644 --- a/app/views/projects/project_members/_new_shared_group.html.haml +++ b/app/views/projects/project_members/_new_shared_group.html.haml @@ -8,7 +8,7 @@ = label_tag :link_group_access, "Max access level", class: "label-light" .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" - = icon('caret-down') + = icon('chevron-down') .help-block.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions diff --git a/changelogs/unreleased/12151-add-since-and-until-params-to-issuables.yml b/changelogs/unreleased/12151-add-since-and-until-params-to-issuables.yml new file mode 100644 index 00000000000..2c915e62357 --- /dev/null +++ b/changelogs/unreleased/12151-add-since-and-until-params-to-issuables.yml @@ -0,0 +1,4 @@ +--- +title: Added "created_after" and "created_before" params to issuables +merge_request: 12151 +author: Kyle Bishop @kybishop diff --git a/changelogs/unreleased/26212-upload-user-avatar-trough-api.yml b/changelogs/unreleased/26212-upload-user-avatar-trough-api.yml new file mode 100644 index 00000000000..667454ae95d --- /dev/null +++ b/changelogs/unreleased/26212-upload-user-avatar-trough-api.yml @@ -0,0 +1,4 @@ +--- +title: Accept image for avatar in user API +merge_request: 12143 +author: Ivan Chernov diff --git a/changelogs/unreleased/27697-make-arrow-icons-consistent-in-dropdown.yml b/changelogs/unreleased/27697-make-arrow-icons-consistent-in-dropdown.yml new file mode 100644 index 00000000000..92b5b59f46f --- /dev/null +++ b/changelogs/unreleased/27697-make-arrow-icons-consistent-in-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Use fa-chevron-down on dropdown arrows for consistency +merge_request: 9659 +author: TM Lee diff --git a/changelogs/unreleased/28139-use-color-input-broadcast-messages.yml b/changelogs/unreleased/28139-use-color-input-broadcast-messages.yml new file mode 100644 index 00000000000..97ebabaff1c --- /dev/null +++ b/changelogs/unreleased/28139-use-color-input-broadcast-messages.yml @@ -0,0 +1,4 @@ +--- +title: Use color inputs for broadcast messages +merge_request: +author: diff --git a/changelogs/unreleased/30725-reset-user-limits-when-unchecking-external-user.yml b/changelogs/unreleased/30725-reset-user-limits-when-unchecking-external-user.yml new file mode 100644 index 00000000000..3058404b3f8 --- /dev/null +++ b/changelogs/unreleased/30725-reset-user-limits-when-unchecking-external-user.yml @@ -0,0 +1,4 @@ +--- +title: Ensures default user limits when external user is unchecked +merge_request: 12218 +author: diff --git a/changelogs/unreleased/33461-display-user-id.yml b/changelogs/unreleased/33461-display-user-id.yml new file mode 100644 index 00000000000..cba94625b07 --- /dev/null +++ b/changelogs/unreleased/33461-display-user-id.yml @@ -0,0 +1,4 @@ +--- +title: Display own user id in account settings page +merge_request: 12141 +author: Riccardo Padovani diff --git a/changelogs/unreleased/issue_33205.yml b/changelogs/unreleased/issue_33205.yml new file mode 100644 index 00000000000..54b442048d8 --- /dev/null +++ b/changelogs/unreleased/issue_33205.yml @@ -0,0 +1,4 @@ +--- +title: Fix API bug accepting wrong parameter to create merge request +merge_request: +author: diff --git a/changelogs/unreleased/moved-submodules.yml b/changelogs/unreleased/moved-submodules.yml new file mode 100644 index 00000000000..eee858717ed --- /dev/null +++ b/changelogs/unreleased/moved-submodules.yml @@ -0,0 +1,4 @@ +--- +title: 'Handle renamed submodules in repository browser' +merge_request: 10798 +author: David Turner diff --git a/changelogs/unreleased/mr-widget-memory-usage-tech-debt-fix.yml b/changelogs/unreleased/mr-widget-memory-usage-tech-debt-fix.yml new file mode 100644 index 00000000000..14b5493a246 --- /dev/null +++ b/changelogs/unreleased/mr-widget-memory-usage-tech-debt-fix.yml @@ -0,0 +1,4 @@ +--- +title: Changed utilities imports from ~ to relative paths +merge_request: +author: diff --git a/changelogs/unreleased/reduce-sidekiq-wait-timings.yml b/changelogs/unreleased/reduce-sidekiq-wait-timings.yml new file mode 100644 index 00000000000..4d23accc82e --- /dev/null +++ b/changelogs/unreleased/reduce-sidekiq-wait-timings.yml @@ -0,0 +1,4 @@ +--- +title: Reduce time spent waiting for certain Sidekiq jobs to complete +merge_request: +author: diff --git a/config/karma.config.js b/config/karma.config.js index 978850e5d70..5911a9a7e10 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -54,6 +54,7 @@ module.exports = function(config) { subdir: '.', fixWebpackSourcePaths: true }; + karmaConfig.browserNoActivityTimeout = 60000; // 60 seconds } if (process.env.DEBUG) { diff --git a/config/webpack.config.js b/config/webpack.config.js index 3c2455ebf35..fb91ffef7e7 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -11,22 +11,13 @@ var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeMod 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 IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var NO_COMPRESSION = process.env.NO_COMPRESSION; -// optional dependency `node-zopfli` is unavailable on CentOS 6 -var ZOPFLI_AVAILABLE; -try { - require.resolve('node-zopfli'); - ZOPFLI_AVAILABLE = true; -} catch(err) { - ZOPFLI_AVAILABLE = false; -} - var config = { // because sqljs requires fs. node: { @@ -233,12 +224,12 @@ if (IS_PRODUCTION) { // zopfli requires a lot of compute time and is disabled in CI if (!NO_COMPRESSION) { - config.plugins.push( - new CompressionPlugin({ - asset: '[path].gz[query]', - algorithm: ZOPFLI_AVAILABLE ? 'zopfli' : 'gzip', - }) - ); + // gracefully fall back to gzip if `node-zopfli` is unavailable (e.g. in CentOS 6) + try { + config.plugins.push(new CompressionPlugin({ algorithm: 'zopfli' })); + } catch(err) { + config.plugins.push(new CompressionPlugin({ algorithm: 'gzip' })); + } } } diff --git a/doc/api/issues.md b/doc/api/issues.md index 3f949ca5667..df5666bb7b6 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -221,7 +221,8 @@ GET /projects/:id/issues?search=issue+title+or+description | `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` | | `search` | string | no | Search project issues against their `title` and `description` | - +| `created_after` | datetime | no | Return issues created after the given time (inclusive) | +| `created_before` | datetime | no | Return issues created before the given time (inclusive) | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index cb22b67f556..3dc808c196d 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -26,6 +26,8 @@ Parameters: | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `milestone` | string | no | Return merge requests for a specific milestone | | `labels` | string | no | Return merge requests matching a comma separated list of labels | +| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) | +| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) | ```json [ diff --git a/doc/api/users.md b/doc/api/users.md index 91ce4f6dac3..b1ebd7b0c47 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -251,6 +251,7 @@ Parameters: - `can_create_group` (optional) - User can create groups - true or false - `confirm` (optional) - Require confirmation - true (default) or false - `external` (optional) - Flags the user as external - true or false(default) +- `avatar` (optional) - Image file for user's avatar ## User modification @@ -279,6 +280,7 @@ Parameters: - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) +- `avatar` (optional) - Image file for user's avatar On password update, user will be forced to change it upon next login. Note, at the moment this method does only return a `404` error, diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 3fda47b9e34..3d47e644ad2 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -89,6 +89,7 @@ group. | Create project in group | | | | ✓ | ✓ | | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | +| Manage group labels | | ✓ | ✓ | ✓ | ✓ | ## External Users diff --git a/doc/user/project/integrations/img/merge_request_performance.png b/doc/user/project/integrations/img/merge_request_performance.png Binary files differindex 93b2626fed7..eba6515a6ae 100644 --- a/doc/user/project/integrations/img/merge_request_performance.png +++ b/doc/user/project/integrations/img/merge_request_performance.png diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index d3fb5916dc6..86ceb14b965 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -167,15 +167,15 @@ environment which has had a successful deployment. ## Determining the performance impact of a merge > [Introduced][ce-10408] in GitLab 9.2. +> GitLab 9.3 added the [numeric comparison](https://gitlab.com/gitlab-org/gitlab-ce/issues/27439) of the 30 minute averages. Developers can view the performance impact of their changes within the merge -request workflow. When a source branch has been deployed to an environment, a -sparkline will appear showing the average memory consumption of the app. The dot +request workflow. When a source branch has been deployed to an environment, a sparkline and numeric comparison of the average memory consumption will appear. On the sparkline, a dot indicates when the current changes were deployed, with up to 30 minutes of -performance data displayed before and after. The sparkline will be updated after +performance data displayed before and after. The comparison shows the difference between the 30 minute average before and after the deployment. This information is updated after each commit has been deployed. -Once merged and the target branch has been redeployed, the sparkline will switch +Once merged and the target branch has been redeployed, the metrics will switch to show the new environments this revision has been deployed to. Performance data will be available for the duration it is persisted on the diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index d3732d67622..5e9cf5e68b1 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -10,6 +10,10 @@ module API set_project unless defined?(@project) @project end + + def redirected_path + @redirected_path + end def ssh_authentication_abilities [ @@ -38,8 +42,9 @@ module API def set_project if params[:gl_repository] @project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository]) + @redirected_path = nil else - @project, @wiki = Gitlab::RepoPath.parse(params[:project]) + @project, @wiki, @redirected_path = Gitlab::RepoPath.parse(params[:project]) end end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index ecd6d672cf7..9ec418edea4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -34,7 +34,7 @@ module API access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess access_checker = access_checker_klass - .new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities) + .new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, redirected_path: redirected_path) begin access_checker.check(params[:action], params[:changes]) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 78db960ae28..09dca0dff8b 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -27,6 +27,8 @@ module API optional :milestone, type: String, desc: 'Return issues for a specific milestone' optional :iids, type: Array[Integer], desc: 'The IID array of issues' optional :search, type: String, desc: 'Search issues for text present in the title or description' + optional :created_after, type: DateTime, desc: 'Return issues created after the specified time' + optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' use :pagination end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 710deba5ae3..1118fc7465b 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -72,6 +72,8 @@ module API optional :iids, type: Array[Integer], desc: 'The IID array of merge requests' optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time' + optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time' use :pagination end get ":id/merge_requests" do @@ -97,7 +99,7 @@ module API authorize! :create_merge_request, user_project mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute diff --git a/lib/api/users.rb b/lib/api/users.rb index dda64715ee1..7257ecb5b67 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -29,6 +29,7 @@ module API optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' + optional :avatar, type: File, desc: 'Avatar image for user' all_or_none_of :extern_uid, :provider end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 48735fd197d..2fd5452dd78 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -62,8 +62,7 @@ module Gitlab active_db_connection = ActiveRecord::Base.connection.active? rescue false active_db_connection && - ActiveRecord::Base.connection.table_exists?('application_settings') && - !ActiveRecord::Migrator.needs_migration? + ActiveRecord::Base.connection.table_exists?('application_settings') rescue ActiveRecord::NoDatabaseError false end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index cd85f961242..9181202a091 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -233,6 +233,12 @@ module Gitlab # Update in batches of 5% until we run out of any rows to update. batch_size = ((total / 100.0) * 5.0).ceil + max_size = 1000 + + # The upper limit is 1000 to ensure we don't lock too many rows. For + # example, for "merge_requests" even 1% of the table is around 35 000 + # rows for GitLab.com. + batch_size = max_size if batch_size > max_size start_arel = table.project(table[:id]).order(table[:id].asc).take(1) start_arel = yield table, start_arel if block_given? diff --git a/lib/gitlab/git/gitmodules_parser.rb b/lib/gitlab/git/gitmodules_parser.rb new file mode 100644 index 00000000000..f4e3b5e5129 --- /dev/null +++ b/lib/gitlab/git/gitmodules_parser.rb @@ -0,0 +1,77 @@ +module Gitlab + module Git + class GitmodulesParser + def initialize(content) + @content = content + end + + # Parses the contents of a .gitmodules file and returns a hash of + # submodule information, indexed by path. + def parse + reindex_by_path(get_submodules_by_name) + end + + private + + class State + def initialize + @result = {} + @current_submodule = nil + end + + def start_section(section) + # In some .gitmodules files (e.g. nodegit's), a header + # with the same name appears multiple times; we want to + # accumulate the configs across these + @current_submodule = @result[section] || { 'name' => section } + @result[section] = @current_submodule + end + + def set_attribute(attr, value) + @current_submodule[attr] = value + end + + def section_started? + !@current_submodule.nil? + end + + def submodules_by_name + @result + end + end + + def get_submodules_by_name + iterator = State.new + + @content.split("\n").each_with_object(iterator) do |text, iterator| + next if text =~ /^\s*#/ + + if text =~ /\A\[submodule "(?<name>[^"]+)"\]\z/ + iterator.start_section($~[:name]) + else + next unless iterator.section_started? + + next unless text =~ /\A\s*(?<key>\w+)\s*=\s*(?<value>.*)\z/ + + value = $~[:value].chomp + iterator.set_attribute($~[:key], value) + end + end + + iterator.submodules_by_name + end + + def reindex_by_path(submodules_by_name) + # Convert from an indexed by name to an array indexed by path + # If a submodule doesn't have a path, it is considered bogus + # and is ignored + submodules_by_name.each_with_object({}) do |(name, data), results| + path = data.delete 'path' + next unless path + + results[path] = data + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 85695d0a4df..c1f942f931a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -617,9 +617,9 @@ module Gitlab # # Ex. # { - # "rack" => { + # "current_path/rack" => { + # "name" => "original_path/rack", # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320", - # "path" => "rack", # "url" => "git://github.com/chneukirchen/rack.git" # }, # "encoding" => { @@ -637,7 +637,8 @@ module Gitlab return {} end - parse_gitmodules(commit, content) + parser = GitmodulesParser.new(content) + fill_submodule_ids(commit, parser.parse) end # Return total commits count accessible from passed ref @@ -998,42 +999,19 @@ module Gitlab end end - # Parses the contents of a .gitmodules file and returns a hash of - # submodule information. - def parse_gitmodules(commit, content) - modules = {} - - name = nil - content.each_line do |line| - case line.strip - when /\A\[submodule "(?<name>[^"]+)"\]\z/ # Submodule header - name = $~[:name] - modules[name] = {} - when /\A(?<key>\w+)\s*=\s*(?<value>.*)\z/ # Key/value pair - key = $~[:key] - value = $~[:value].chomp - - next unless name && modules[name] - - modules[name][key] = value - - if key == 'path' - begin - modules[name]['id'] = blob_content(commit, value) - rescue InvalidBlobName - # The current entry is invalid - modules.delete(name) - name = nil - end - end - when /\A#/ # Comment - next - else # Invalid line - name = nil + # Fill in the 'id' field of a submodule hash from its values + # as-of +commit+. Return a Hash consisting only of entries + # from the submodule hash for which the 'id' field is filled. + def fill_submodule_ids(commit, submodule_data) + submodule_data.each do |path, data| + id = begin + blob_content(commit, path) + rescue InvalidBlobName + nil end + data['id'] = id end - - modules + submodule_data.select { |path, data| data['id'] } end # Returns true if +commit+ introduced changes to +path+, using commit diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 0a19d24eb20..0b62911958d 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -22,12 +22,13 @@ module Gitlab PUSH_COMMANDS = %w{ git-receive-pack }.freeze ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS - attr_reader :actor, :project, :protocol, :authentication_abilities + attr_reader :actor, :project, :protocol, :authentication_abilities, :redirected_path - def initialize(actor, project, protocol, authentication_abilities:) + def initialize(actor, project, protocol, authentication_abilities:, redirected_path: nil) @actor = actor @project = project @protocol = protocol + @redirected_path = redirected_path @authentication_abilities = authentication_abilities end @@ -35,6 +36,7 @@ module Gitlab check_protocol! check_active_user! check_project_accessibility! + check_project_moved! check_command_disabled!(cmd) check_command_existence!(cmd) check_repository_existence! @@ -87,6 +89,21 @@ module Gitlab end end + def check_project_moved! + if redirected_path + url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo + message = <<-MESSAGE.strip_heredoc + Project '#{redirected_path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{url} + MESSAGE + + raise NotFoundError, message + end + end + def check_command_disabled!(cmd) if upload_pack?(cmd) check_upload_pack_disabled! diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb index 8db91d25a4b..208f0e1bbea 100644 --- a/lib/gitlab/job_waiter.rb +++ b/lib/gitlab/job_waiter.rb @@ -14,7 +14,7 @@ module Gitlab # timeout - The maximum amount of seconds to block the caller for. This # ensures we don't indefinitely block a caller in case a job takes # long to process, or is never processed. - def wait(timeout = 60) + def wait(timeout = 10) start = Time.current while (Time.current - start) <= timeout diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 878e03f61d7..3591fa9145e 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -3,16 +3,18 @@ module Gitlab NotFoundError = Class.new(StandardError) def self.parse(repo_path) + wiki = false project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false) - project = Project.find_by_full_path(project_path) - if project_path.end_with?('.wiki') && !project - project = Project.find_by_full_path(project_path.chomp('.wiki')) + project, was_redirected = find_project(project_path) + + if project_path.end_with?('.wiki') && project.nil? + project, was_redirected = find_project(project_path.chomp('.wiki')) wiki = true - else - wiki = false end - [project, wiki] + redirected_path = project_path if was_redirected + + [project, wiki, redirected_path] end def self.strip_storage_path(repo_path, fail_on_not_found: true) @@ -30,5 +32,12 @@ module Gitlab result.sub(/\A\/*/, '') end + + def self.find_project(project_path) + project = Project.find_by_full_path(project_path, follow_redirects: true) + was_redirected = project && project.full_path.casecmp(project_path) != 0 + + [project, was_redirected] + end end end diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb deleted file mode 100644 index 9e117c8ed9f..00000000000 --- a/spec/features/milestones/milestones_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# require 'rails_helper' - -# describe 'Milestone draggable', feature: true, js: true do -# include DragTo - -# let(:milestone) { create(:milestone, project: project, title: 8.14) } -# let(:project) { create(:empty_project, :public) } -# let(:user) { create(:user) } - -# context 'issues' do -# let(:issue) { page.find_by_id('issues-list-unassigned').find('li') } -# let(:issue_target) { page.find_by_id('issues-list-ongoing') } - -# it 'does not allow guest to drag issue' do -# create_and_drag_issue - -# expect(issue_target).not_to have_selector('.issuable-row') -# end - -# it 'does not allow authorized user to drag issue' do -# login_as(user) -# create_and_drag_issue - -# expect(issue_target).not_to have_selector('.issuable-row') -# end - -# it 'allows author to drag issue' do -# login_as(user) -# create_and_drag_issue(author: user) - -# expect(issue_target).to have_selector('.issuable-row') -# end - -# it 'allows admin to drag issue' do -# login_as(:admin) - -# create_and_drag_issue - -# expect(issue_target).to have_selector('.issuable-row') -# end -# end - -# context 'merge requests' do -# let(:merge_request) { page.find_by_id('merge_requests-list-unassigned').find('li') } -# let(:merge_request_target) { page.find_by_id('merge_requests-list-ongoing') } - -# it 'does not allow guest to drag merge request' do -# create_and_drag_merge_request - -# expect(merge_request_target).not_to have_selector('.issuable-row') -# end - -# it 'does not allow authorized user to drag merge request' do -# login_as(user) -# create_and_drag_merge_request - -# expect(merge_request_target).not_to have_selector('.issuable-row') -# end - -# it 'allows author to drag merge request' do -# login_as(user) -# create_and_drag_merge_request(author: user) - -# expect(merge_request_target).to have_selector('.issuable-row') -# end - -# it 'allows admin to drag merge request' do -# login_as(:admin) -# create_and_drag_merge_request - -# expect(merge_request_target).to have_selector('.issuable-row') -# end -# end - -# def create_and_drag_issue(params = {}) -# create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone)) - -# visit namespace_project_milestone_path(project.namespace, project, milestone) -# scroll_into_view('.milestone-content') -# drag_to(selector: '.issues-sortable-list', list_to_index: 1) - -# wait_for_requests -# end - -# def create_and_drag_merge_request(params = {}) -# create(:merge_request, params.merge(title: 'Foo', source_project: project, target_project: project, milestone: milestone)) - -# visit namespace_project_milestone_path(project.namespace, project, milestone) -# page.find("a[href='#tab-merge-requests']").click - -# wait_for_requests - -# scroll_into_view('.milestone-content') -# drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1) - -# wait_for_requests -# end - -# def scroll_into_view(selector) -# page.evaluate_script("document.querySelector('#{selector}').scrollIntoView();") -# end -# end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 8f2d60f2f1b..8ace1fb5751 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -7,9 +7,9 @@ describe IssuesFinder do set(:project2) { create(:empty_project) } set(:milestone) { create(:milestone, project: project1) } set(:label) { create(:label, project: project2) } - set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab') } + set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) } set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') } - set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki') } + set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) } describe '#execute' do set(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } @@ -215,6 +215,24 @@ describe IssuesFinder do end end + context 'filtering by created_at' do + context 'through created_after' do + let(:params) { { created_after: issue3.created_at } } + + it 'returns issues created on or after the given date' do + expect(issues).to contain_exactly(issue3) + end + end + + context 'through created_before' do + let(:params) { { created_before: issue1.created_at + 1.second } } + + it 'returns issues created on or before the given date' do + expect(issues).to contain_exactly(issue1) + end + end + end + context 'when the user is unauthorized' do let(:search_user) { nil } diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 58b7cd5e098..5eb26de6c92 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -46,5 +46,47 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request1) end + + context 'with created_after and created_before params' do + let(:project4) { create(:empty_project, forked_from_project: project1) } + + let!(:new_merge_request) do + create(:merge_request, + :simple, + author: user, + created_at: 1.week.from_now, + source_project: project4, + target_project: project1) + end + + let!(:old_merge_request) do + create(:merge_request, + :simple, + author: user, + created_at: 1.week.ago, + source_project: project4, + target_project: project4) + end + + before do + project4.add_master(user) + end + + it 'filters by created_after' do + params = { project_id: project1.id, created_after: new_merge_request.created_at } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(new_merge_request) + end + + it 'filters by created_before' do + params = { project_id: project4.id, created_before: old_merge_request.created_at + 1.second } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request) + end + end end end diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 3dba2e817ff..cc336180ff7 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,4 +1,3 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */ /* global Project */ import 'select2/select2'; @@ -7,47 +6,52 @@ import '~/api'; import '~/project_select'; import '~/project'; -(function() { - describe('Project Title', function() { - preloadFixtures('issues/open-issue.html.raw'); - loadJSONFixtures('projects.json'); +describe('Project Title', () => { + preloadFixtures('issues/open-issue.html.raw'); + loadJSONFixtures('projects.json'); - beforeEach(function() { - loadFixtures('issues/open-issue.html.raw'); + beforeEach(() => { + loadFixtures('issues/open-issue.html.raw'); - window.gon = {}; - window.gon.api_version = 'v3'; + window.gon = {}; + window.gon.api_version = 'v3'; - return this.project = new Project(); - }); + // eslint-disable-next-line no-new + new Project(); + }); - describe('project list', function() { - var fakeAjaxResponse = function fakeAjaxResponse(req) { - var d; - expect(req.url).toBe('/api/v3/projects.json?simple=true'); - expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true }); - d = $.Deferred(); - d.resolve(this.projects_data); - return d.promise(); - }; - - beforeEach((function(_this) { - return function() { - _this.projects_data = getJSONFixture('projects.json'); - return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); - }; - })(this)); - it('toggles dropdown', function() { - var menu = $('.js-dropdown-menu-projects'); - $('.js-projects-dropdown-toggle').click(); - expect(menu).toHaveClass('open'); - menu.find('.dropdown-menu-close-icon').click(); - expect(menu).not.toHaveClass('open'); + describe('project list', () => { + let reqUrl; + let reqData; + + beforeEach(() => { + const fakeResponseData = getJSONFixture('projects.json'); + spyOn(jQuery, 'ajax').and.callFake((req) => { + const def = $.Deferred(); + reqUrl = req.url; + reqData = req.data; + def.resolve(fakeResponseData); + return def.promise(); }); }); - afterEach(() => { - window.gon = {}; + it('toggles dropdown', () => { + const $menu = $('.js-dropdown-menu-projects'); + $('.js-projects-dropdown-toggle').click(); + expect($menu).toHaveClass('open'); + expect(reqUrl).toBe('/api/v3/projects.json?simple=true'); + expect(reqData).toEqual({ + search: '', + order_by: 'last_activity_at', + per_page: 20, + membership: true, + }); + $menu.find('.dropdown-menu-close-icon').click(); + expect($menu).not.toHaveClass('open'); }); }); -}).call(window); + + afterEach(() => { + window.gon = {}; + }); +}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 2c34402576b..729db02e06c 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -16,6 +16,14 @@ window.gl = window.gl || {}; window.gl.TEST_HOST = 'http://test.host'; window.gon = window.gon || {}; +// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row +// because it appears to lock up the thread that communicates to Karma's socket +// This async beforeEach gets called on every spec and releases the JS thread long +// enough for the socket to continue to communicate. +// The downside is that it creates a minor performance penalty in the time it takes +// to run our unit tests. +beforeEach(done => done()); // eslint-disable-line jasmine/no-global-setup + // render all of our tests const testsContext = require.context('.', true, /_spec$/); testsContext.keys().forEach(function (path) { diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb new file mode 100644 index 00000000000..143aa2218c9 --- /dev/null +++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Git::GitmodulesParser do + it 'should parse a .gitmodules file correctly' do + parser = described_class.new(<<-'GITMODULES'.strip_heredoc) + [submodule "vendor/libgit2"] + path = vendor/libgit2 + [submodule "vendor/libgit2"] + url = https://github.com/nodegit/libgit2.git + + # a comment + [submodule "moved"] + path = new/path + url = https://example.com/some/project + [submodule "bogus"] + url = https://example.com/another/project + GITMODULES + + modules = parser.parse + + expect(modules).to eq({ + 'vendor/libgit2' => { 'name' => 'vendor/libgit2', + 'url' => 'https://github.com/nodegit/libgit2.git' }, + 'new/path' => { 'name' => 'moved', + 'url' => 'https://example.com/some/project' } + }) + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index eee4c9eab6d..02b3f167250 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -358,7 +358,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(submodule).to eq([ "six", { "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", - "path" => "six", + "name" => "six", "url" => "git://github.com/randx/six.git" } ]) @@ -366,14 +366,14 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'should handle nested submodules correctly' do nested = submodules['nested/six'] - expect(nested['path']).to eq('nested/six') + expect(nested['name']).to eq('nested/six') expect(nested['url']).to eq('git://github.com/randx/six.git') expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') end it 'should handle deeply nested submodules correctly' do nested = submodules['deeper/nested/six'] - expect(nested['path']).to eq('deeper/nested/six') + expect(nested['name']).to eq('deeper/nested/six') expect(nested['url']).to eq('git://github.com/randx/six.git') expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') end @@ -393,7 +393,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(submodules.first).to eq([ "six", { "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", - "path" => "six", + "name" => "six", "url" => "git://github.com/randx/six.git" } ]) diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 3dcc20c48e8..9a86cfa66e4 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' describe Gitlab::GitAccess, lib: true do let(:pull_access_check) { access.check('git-upload-pack', '_any') } let(:push_access_check) { access.check('git-receive-pack', '_any') } - let(:access) { Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: authentication_abilities) } + let(:access) { Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:actor) { user } let(:protocol) { 'ssh' } + let(:redirected_path) { nil } let(:authentication_abilities) do [ :read_project, @@ -162,6 +163,46 @@ describe Gitlab::GitAccess, lib: true do end end + describe '#check_project_moved!' do + before do + project.team << [user, :master] + end + + context 'when a redirect was not followed to find the project' do + context 'pull code' do + it { expect { pull_access_check }.not_to raise_error } + end + + context 'push code' do + it { expect { push_access_check }.not_to raise_error } + end + end + + context 'when a redirect was followed to find the project' do + let(:redirected_path) { 'some/other-path' } + + context 'pull code' do + it { expect { pull_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) } + it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) } + + context 'http protocol' do + let(:protocol) { 'http' } + it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) } + end + end + + context 'push code' do + it { expect { push_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) } + it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) } + + context 'http protocol' do + let(:protocol) { 'http' } + it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) } + end + end + end + end + describe '#check_command_disabled!' do before do project.team << [user, :master] diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index a1eb95750ba..797ec8cb23e 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -1,9 +1,10 @@ require 'spec_helper' describe Gitlab::GitAccessWiki, lib: true do - let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) } + let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:project) { create(:project, :repository) } let(:user) { create(:user) } + let(:redirected_path) { nil } let(:authentication_abilities) do [ :read_project, diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index f9025397107..efea4f429bf 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -4,24 +4,44 @@ describe ::Gitlab::RepoPath do describe '.parse' do set(:project) { create(:project) } - it 'parses a full repository path' do - expect(described_class.parse(project.repository.path)).to eq([project, false]) - end + context 'a repository storage path' do + it 'parses a full repository path' do + expect(described_class.parse(project.repository.path)).to eq([project, false, nil]) + end - it 'parses a full wiki path' do - expect(described_class.parse(project.wiki.repository.path)).to eq([project, true]) + it 'parses a full wiki path' do + expect(described_class.parse(project.wiki.repository.path)).to eq([project, true, nil]) + end end - it 'parses a relative repository path' do - expect(described_class.parse(project.full_path + '.git')).to eq([project, false]) - end + context 'a relative path' do + it 'parses a relative repository path' do + expect(described_class.parse(project.full_path + '.git')).to eq([project, false, nil]) + end - it 'parses a relative wiki path' do - expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true]) - end + it 'parses a relative wiki path' do + expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true, nil]) + end + + it 'parses a relative path starting with /' do + expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false, nil]) + end + + context 'of a redirected project' do + let(:redirect) { project.route.create_redirect('foo/bar') } + + it 'parses a relative repository path' do + expect(described_class.parse(redirect.path + '.git')).to eq([project, false, 'foo/bar']) + end + + it 'parses a relative wiki path' do + expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, true, 'foo/bar.wiki']) + end - it 'parses a relative path starting with /' do - expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false]) + it 'parses a relative path starting with /' do + expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, false, 'foo/bar']) + end + end end end @@ -43,4 +63,33 @@ describe ::Gitlab::RepoPath do ) end end + + describe '.find_project' do + let(:project) { create(:empty_project) } + let(:redirect) { project.route.create_redirect('foo/bar/baz') } + + context 'when finding a project by its canonical path' do + context 'when the cases match' do + it 'returns the project and false' do + expect(described_class.find_project(project.full_path)).to eq([project, false]) + end + end + + context 'when the cases do not match' do + # This is slightly different than web behavior because on the web it is + # easy and safe to redirect someone to the correctly-cased URL. For git + # requests, we should accept wrongly-cased URLs because it is a pain to + # block people's git operations and force them to update remote URLs. + it 'returns the project and false' do + expect(described_class.find_project(project.full_path.upcase)).to eq([project, false]) + end + end + end + + context 'when finding a project via a redirect' do + it 'returns the project and true' do + expect(described_class.find_project(redirect.path)).to eq([project, true]) + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1a1bbd60504..f6b7120e2c9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -451,6 +451,40 @@ describe User, models: true do end end + describe '#ensure_user_rights_and_limits' do + describe 'with external user' do + let(:user) { create(:user, external: true) } + + it 'receives callback when external changes' do + expect(user).to receive(:ensure_user_rights_and_limits) + + user.update_attributes(external: false) + end + + it 'ensures correct rights and limits for user' do + stub_config_setting(default_can_create_group: true) + + expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true) + .and change { user.projects_limit }.to(current_application_settings.default_projects_limit) + end + end + + describe 'without external user' do + let(:user) { create(:user, external: false) } + + it 'receives callback when external changes' do + expect(user).to receive(:ensure_user_rights_and_limits) + + user.update_attributes(external: true) + end + + it 'ensures correct rights and limits for user' do + expect { user.update_attributes(external: true) }.to change { user.can_create_group }.to(false) + .and change { user.projects_limit }.to(0) + end + end + end + describe 'rss token' do it 'ensures an rss token on read' do user = create(:user, rss_token: nil) diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 86e15d896df..6deaea956e0 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -321,8 +321,6 @@ describe API::Internal do end context "archived project" do - let(:personal_project) { create(:empty_project, namespace: user.namespace) } - before do project.team << [user, :developer] project.archive! @@ -445,6 +443,42 @@ describe API::Internal do expect(json_response['status']).to be_truthy end end + + context 'the project path was changed' do + let!(:old_path_to_repo) { project.repository.path_to_repo } + let!(:old_full_path) { project.full_path } + let(:project_moved_message) do + <<-MSG.strip_heredoc + Project '#{old_full_path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{project.ssh_url_to_repo} + MSG + end + + before do + project.team << [user, :developer] + project.path = 'new_path' + project.save! + end + + it 'rejects the push' do + push_with_path(key, old_path_to_repo) + + expect(response).to have_http_status(200) + expect(json_response['status']).to be_falsey + expect(json_response['message']).to eq(project_moved_message) + end + + it 'rejects the SSH pull' do + pull_with_path(key, old_path_to_repo) + + expect(response).to have_http_status(200) + expect(json_response['status']).to be_falsey + expect(json_response['message']).to eq(project_moved_message) + end + end end describe 'GET /internal/merge_request_urls' do @@ -587,6 +621,17 @@ describe API::Internal do ) end + def pull_with_path(key, path_to_repo, protocol = 'ssh') + post( + api("/internal/allowed"), + key_id: key.id, + project: path_to_repo, + action: 'git-upload-pack', + secret_token: secret_token, + protocol: protocol + ) + end + def push(key, project, protocol = 'ssh', env: nil) post( api("/internal/allowed"), @@ -600,6 +645,19 @@ describe API::Internal do ) end + def push_with_path(key, path_to_repo, protocol = 'ssh', env: nil) + post( + api("/internal/allowed"), + changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master', + key_id: key.id, + project: path_to_repo, + action: 'git-receive-pack', + secret_token: secret_token, + protocol: protocol, + env: env + ) + end + def archive(key, project) post( api("/internal/allowed"), diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 16e5efb2f5b..452ff4aba8e 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -334,14 +334,13 @@ describe API::MergeRequests do target_branch: 'master', author: user, labels: 'label, label2', - milestone_id: milestone.id, - remove_source_branch: true + milestone_id: milestone.id expect(response).to have_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['milestone']['id']).to eq(milestone.id) - expect(json_response['force_remove_source_branch']).to be_truthy + expect(json_response['force_remove_source_branch']).to be_falsy end it "returns 422 when source_branch equals target_branch" do @@ -404,6 +403,27 @@ describe API::MergeRequests do expect(response).to have_http_status(409) end end + + context 'accepts remove_source_branch parameter' do + let(:params) do + { title: 'Test merge_request', + source_branch: 'markdown', + target_branch: 'master', + author: user } + end + + it 'sets force_remove_source_branch to false' do + post api("/projects/#{project.id}/merge_requests", user), params.merge(remove_source_branch: false) + + expect(json_response['force_remove_source_branch']).to be_falsy + end + + it 'sets force_remove_source_branch to true' do + post api("/projects/#{project.id}/merge_requests", user), params.merge(remove_source_branch: true) + + expect(json_response['force_remove_source_branch']).to be_truthy + end + end end context 'forked projects' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 9dc4b6972a6..bc869ea1108 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -377,6 +377,16 @@ describe API::Users do expect(user.reload.organization).to eq('GitLab') end + it 'updates user with avatar' do + put api("/users/#{user.id}", admin), { avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + + user.reload + + expect(user.avatar).to be_present + expect(response).to have_http_status(200) + expect(json_response['avatar_url']).to include(user.avatar_path) + end + it 'updates user with his own email' do put api("/users/#{user.id}", admin), email: user.email expect(response).to have_http_status(200) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index dce78faefc9..b08148eca3c 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -316,6 +316,26 @@ describe 'Git HTTP requests', lib: true do it_behaves_like 'pushes require Basic HTTP Authentication' end end + + context 'and the user requests a redirected path' do + let!(:redirect) { project.route.create_redirect('foo/bar') } + let(:path) { "#{redirect.path}.git" } + let(:project_moved_message) do + <<-MSG.strip_heredoc + Project '#{redirect.path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{project.http_url_to_repo} + MSG + end + + it 'downloads get status 404 with "project was moved" message' do + clone_get(path, {}) + expect(response).to have_http_status(:not_found) + expect(response.body).to match(project_moved_message) + end + end end context "when the project is private" do @@ -505,6 +525,33 @@ describe 'Git HTTP requests', lib: true do Rack::Attack::Allow2Ban.reset(ip, options) end end + + context 'and the user requests a redirected path' do + let!(:redirect) { project.route.create_redirect('foo/bar') } + let(:path) { "#{redirect.path}.git" } + let(:project_moved_message) do + <<-MSG.strip_heredoc + Project '#{redirect.path}' was moved to '#{project.full_path}'. + + Please update your Git remote and try again: + + git remote set-url origin #{project.http_url_to_repo} + MSG + end + + it 'downloads get status 404 with "project was moved" message' do + clone_get(path, env) + expect(response).to have_http_status(:not_found) + expect(response.body).to match(project_moved_message) + end + + it 'uploads get status 404 with "project was moved" message' do + upload(path, env) do |response| + expect(response).to have_http_status(:not_found) + expect(response.body).to match(project_moved_message) + end + end + end end context "when the user doesn't have access to the project" do @@ -680,7 +727,7 @@ describe 'Git HTTP requests', lib: true do end context "POST git-receive-pack" do - it "failes to find a route" do + it "fails to find a route" do expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) end end diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/profiles/show.html.haml_spec.rb new file mode 100644 index 00000000000..e89a8cb9626 --- /dev/null +++ b/spec/views/profiles/show.html.haml_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe 'profiles/show' do + let(:user) { create(:user) } + + before do + assign(:user, user) + allow(controller).to receive(:current_user).and_return(user) + end + + context 'when the profile page is opened' do + it 'displays the correct elements' do + render + + expect(rendered).to have_field('user_name', user.name) + expect(rendered).to have_field('user_id', user.id) + end + end +end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 3c93da63f2e..6c4df7350ea 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -32,7 +32,7 @@ describe PostReceive do context "with an absolute path as the project identifier" do it "searches the project by full path" do - expect(Project).to receive(:find_by_full_path).with(project.full_path).and_call_original + expect(Project).to receive(:find_by_full_path).with(project.full_path, follow_redirects: true).and_call_original described_class.new.perform(pwd(project), key_id, base64_changes) end |