diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-08-30 19:51:51 +0100 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-08-30 19:51:51 +0100 |
commit | b6502103d4fd08a4046907b4b076b3611f017cb1 (patch) | |
tree | 784bb8e46e66a7e9a6b646fbbbef19b824cf03e7 | |
parent | 9d25efd5c0c37982a16c3636e27cd3e5da0d7fdb (diff) | |
parent | 81f08d30e641dc1a6666022ab1f5d36dbcdced7e (diff) | |
download | gitlab-ce-36917-branch-tooltip.tar.gz |
Merge branch 'master' into 36917-branch-tooltip36917-branch-tooltip
* master: (26 commits)
Fix invalid attribute used for time-ago-tooltip component
Update latest artifacts doc
Update share project with groups docs
Remove tooltips from new sidebar
Use `git update-ref --stdin -z` to delete refs
Don't use public_send in destroy_conditionally! helper
Fix MySQL failure for emoji autocomplete
Replace 'project/user_lookup.feature' spinach test with an rspec analog
Add changelog
Add time stats to issue and merge request API end points
Resolve new N+1 by adding preloads and metadata to issues end points
Add missing N+1 test to issues spec
Add time stats to API schema for issue and merge request end points
Add time stats documentation to issue and merge request end points
Improve migrations using triggers
Increase z-index of dropdowns
Just use a block
Fix tests
Try to make reserved ref names more obvious
Resolve feedback on the MR:
...
51 files changed, 675 insertions, 163 deletions
diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue index d59e6d11032..992b7064c13 100644 --- a/app/assets/javascripts/issue_show/components/edited.vue +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -37,7 +37,7 @@ export default { Edited <time-ago-tooltip v-if="updatedAt" - placement="bottom" + tooltip-placement="bottom" :time="updatedAt" /> <span diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index b18d12b48b5..05e3f33f5ed 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -15,6 +15,7 @@ export default class NewNavSidebar { this.$openSidebar = $('.toggle-mobile-nav'); this.$closeSidebar = $('.close-nav-button'); this.$sidebarToggle = $('.js-toggle-sidebar'); + this.$topLevelLinks = $('.sidebar-top-level-items > li > a'); } bindEvents() { @@ -50,6 +51,10 @@ export default class NewNavSidebar { this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); + + this.$topLevelLinks.attr('title', function updateTopLevelTitle() { + return collapsed ? this.getAttribute('aria-label') : ''; + }); } render() { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 4cf2f46c6d3..1024295bbd6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -189,7 +189,7 @@ width: auto; top: 100%; left: 0; - z-index: 9; + z-index: 200; min-width: 240px; max-width: 500px; margin-top: 2px; diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 54f78fc8719..59be955599d 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -55,7 +55,7 @@ class AutocompleteController < ApplicationController .limit(AWARD_EMOJI_MAX) .where(user: current_user) .group(:name) - .order(count: :desc, name: :asc) + .order('count_all DESC, name ASC') .count # Transform from hash to array to guarantee json order diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index ee701076a14..3308ab0c259 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -119,4 +119,8 @@ module TabHelper 'active' if current_controller?('oauth/applications') end + + def sidebar_link(href, title: nil, css: nil, &block) + link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title } + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index e9ebf0637f3..435eeaf0e2e 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -114,7 +114,7 @@ class Environment < ActiveRecord::Base end def ref_path - "refs/environments/#{Shellwords.shellescape(name)}" + "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}" end def formatted_external_url diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7f73de67625..5be2f6d4e82 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -803,7 +803,7 @@ class MergeRequest < ActiveRecord::Base end def ref_path - "refs/merge-requests/#{iid}/head" + "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head" end def ref_fetched? diff --git a/app/models/project.rb b/app/models/project.rb index d5324ceac31..9d7bea4eb66 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -372,11 +372,7 @@ class Project < ActiveRecord::Base if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists? project.run_after_commit do - begin - Projects::HousekeepingService.new(project).execute - rescue Projects::HousekeepingService::LeaseTaken => e - Rails.logger.info("Could not perform housekeeping for project #{project.full_path} (#{project.id}): #{e}") - end + Projects::AfterImportService.new(project).execute end end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 090e3c896bd..d29d2a83708 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,6 +1,18 @@ require 'securerandom' class Repository + REF_MERGE_REQUEST = 'merge-requests'.freeze + REF_KEEP_AROUND = 'keep-around'.freeze + REF_ENVIRONMENTS = 'environments'.freeze + + RESERVED_REFS_NAMES = %W[ + heads + tags + #{REF_ENVIRONMENTS} + #{REF_KEEP_AROUND} + #{REF_ENVIRONMENTS} + ].freeze + include Gitlab::ShellAdapter include RepositoryMirroring @@ -242,10 +254,10 @@ class Repository begin write_ref(keep_around_ref_name(sha), sha) rescue Rugged::ReferenceError => ex - Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" + Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" rescue Rugged::OSError => ex raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ - Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" + Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" end end @@ -1164,7 +1176,7 @@ class Repository end def keep_around_ref_name(sha) - "refs/keep-around/#{sha}" + "refs/#{REF_KEEP_AROUND}/#{sha}" end def repository_event(event, tags = {}) @@ -1234,6 +1246,6 @@ class Repository yield commit(sha) ensure - rugged.references.delete(tmp_ref) if tmp_ref + delete_refs(tmp_ref) if tmp_ref end end diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb new file mode 100644 index 00000000000..e6a68d983ef --- /dev/null +++ b/app/services/projects/after_import_service.rb @@ -0,0 +1,29 @@ +module Projects + class AfterImportService + RESERVED_REFS_REGEXP = + %r{\Arefs/(?:#{Regexp.union(*Repository::RESERVED_REFS_NAMES)})/} + + def initialize(project) + @project = project + end + + def execute + Projects::HousekeepingService.new(@project).execute do + repository.delete_refs(*garbage_refs) + end + rescue Projects::HousekeepingService::LeaseTaken => e + Rails.logger.info( + "Could not perform housekeeping for project #{@project.full_path} (#{@project.id}): #{e}") + end + + private + + def garbage_refs + @garbage_refs ||= repository.all_ref_names_except(RESERVED_REFS_REGEXP) + end + + def repository + @repository ||= @project.repository + end + end +end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index d66ef676088..dcef8b66215 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -26,6 +26,8 @@ module Projects lease_uuid = try_obtain_lease raise LeaseTaken unless lease_uuid.present? + yield if block_given? + execute_gitlab_shell_gc(lease_uuid) end diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml index 9294529f496..3b53117deb6 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -7,7 +7,7 @@ .sidebar-context-title Admin Area %ul.sidebar-top-level-items = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do + = sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do .nav-icon-container = custom_icon('overview') %span.nav-item-name @@ -48,7 +48,7 @@ ConvDev Index = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_conversational_development_index_path, title: 'Monitoring' do + = sidebar_link admin_conversational_development_index_path, title: _('Monitoring') do .nav-icon-container = custom_icon('monitoring') %span.nav-item-name @@ -77,28 +77,28 @@ Requests Profiles = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do + = sidebar_link admin_broadcast_messages_path, title: _('Messages') do .nav-icon-container = custom_icon('messages') %span.nav-item-name Messages = nav_link(controller: [:hooks, :hook_logs]) do - = link_to admin_hooks_path, title: 'Hooks' do + = sidebar_link admin_hooks_path, title: _('Hooks') do .nav-icon-container = custom_icon('system_hooks') %span.nav-item-name System Hooks = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do + = sidebar_link admin_applications_path, title: _('Applications') do .nav-icon-container = custom_icon('applications') %span.nav-item-name Applications = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do + = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do .nav-icon-container = custom_icon('abuse_reports') %span.nav-item-name @@ -107,42 +107,42 @@ - if akismet_enabled? = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do + = sidebar_link admin_spam_logs_path, title: _("Spam Logs") do .nav-icon-container = custom_icon('spam_logs') %span.nav-item-name Spam Logs = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do .nav-icon-container = custom_icon('key') %span.nav-item-name Deploy Keys = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do + = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do .nav-icon-container = custom_icon('service_templates') %span.nav-item-name Service Templates = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do + = sidebar_link admin_labels_path, title: _('Labels') do .nav-icon-container = custom_icon('labels') %span.nav-item-name Labels = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do + = sidebar_link admin_appearances_path, title: _('Appearances') do .nav-icon-container = custom_icon('appearance') %span.nav-item-name Appearance = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do + = sidebar_link admin_application_settings_path, title: _('Settings') do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index d90aea2e361..5a1511b262f 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -8,7 +8,7 @@ = @group.name %ul.sidebar-top-level-items = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group overview' do + = sidebar_link group_path(@group), title: _('Group overview') do .nav-icon-container = custom_icon('project') %span.nav-item-name @@ -26,7 +26,7 @@ Activity = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group), title: 'Issues' do + = sidebar_link issues_group_path(@group), title: _('Issues') do .nav-icon-container = custom_icon('issues') %span.nav-item-name @@ -51,7 +51,7 @@ Milestones = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + = sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do .nav-icon-container = custom_icon('mr_bold') %span.nav-item-name @@ -59,14 +59,14 @@ Merge Requests %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do + = sidebar_link group_group_members_path(@group), title: _('Members') do .nav-icon-container = custom_icon('members') %span.nav-item-name Members - if current_user && can?(current_user, :admin_group, @group) = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = link_to edit_group_path(@group), title: 'Settings' do + = sidebar_link edit_group_path(@group), title: _('Settings') do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml index 85b2c7630c8..ccb6d1492f1 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -7,76 +7,76 @@ .sidebar-context-title User Settings %ul.sidebar-top-level-items = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = link_to profile_path, title: 'Profile Settings' do + = sidebar_link profile_path, title: _('Profile Settings') do .nav-icon-container = custom_icon('profile') %span.nav-item-name Profile = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account' do + = sidebar_link profile_account_path, title: _('Account') do .nav-icon-container = custom_icon('account') %span.nav-item-name Account - if current_application_settings.user_oauth_applications? = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do + = sidebar_link applications_profile_path, title: _('Applications') do .nav-icon-container = custom_icon('applications') %span.nav-item-name Applications = nav_link(controller: :chat_names) do - = link_to profile_chat_names_path, title: 'Chat' do + = sidebar_link profile_chat_names_path, title: _('Chat') do .nav-icon-container = custom_icon('chat') %span.nav-item-name Chat = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do + = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do .nav-icon-container = custom_icon('access_tokens') %span.nav-item-name Access Tokens = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do + = sidebar_link profile_emails_path, title: _('Emails') do .nav-icon-container = custom_icon('emails') %span.nav-item-name Emails - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do + = sidebar_link edit_profile_password_path, title: _('Password') do .nav-icon-container = custom_icon('lock') %span.nav-item-name Password = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do + = sidebar_link profile_notifications_path, title: _('Notifications') do .nav-icon-container = custom_icon('notifications') %span.nav-item-name Notifications = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do + = sidebar_link profile_keys_path, title: _('SSH Keys') do .nav-icon-container = custom_icon('key') %span.nav-item-name SSH Keys = nav_link(controller: :gpg_keys) do - = link_to profile_gpg_keys_path, title: 'GPG Keys' do + = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do .nav-icon-container = custom_icon('key_2') %span.nav-item-name GPG Keys = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences' do + = sidebar_link profile_preferences_path, title: _('Preferences') do .nav-icon-container = custom_icon('preferences') %span.nav-item-name Preferences = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Authentication log' do + = sidebar_link audit_log_profile_path, title: _('Authentication log') do .nav-icon-container = custom_icon('authentication_log') %span.nav-item-name diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index 341943cf833..53dbf9e2f2b 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -9,7 +9,7 @@ = @project.name %ul.sidebar-top-level-items = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do + = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do .nav-icon-container = custom_icon('project') %span.nav-item-name @@ -31,7 +31,7 @@ - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + = sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do .nav-icon-container = custom_icon('doc_text') %span.nav-item-name @@ -72,7 +72,7 @@ - if project_nav_tab? :container_registry = nav_link(controller: %w[projects/registry/repositories]) do - = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + = sidebar_link project_container_registry_index_path(@project), title: _('Container Registry'), css: 'shortcuts-container-registry' do .nav-icon-container = custom_icon('container_registry') %span.nav-item-name @@ -80,7 +80,7 @@ - if project_nav_tab? :issues = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do + = sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do .nav-icon-container = custom_icon('issues') %span.nav-item-name @@ -112,7 +112,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = sidebar_link project_merge_requests_path(@project), title: _('Merge Requests'), css: 'shortcuts-merge_requests' do .nav-icon-container = custom_icon('mr_bold') %span.nav-item-name @@ -122,7 +122,7 @@ - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do + = sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do .nav-icon-container = custom_icon('pipeline') %span.nav-item-name @@ -161,7 +161,7 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do .nav-icon-container = custom_icon('wiki') %span.nav-item-name @@ -169,7 +169,7 @@ - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do + = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do .nav-icon-container = custom_icon('snippets') %span.nav-item-name @@ -177,7 +177,7 @@ - if project_nav_tab? :settings = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do - = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + = sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml b/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml new file mode 100644 index 00000000000..129cf505a3f --- /dev/null +++ b/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml @@ -0,0 +1,4 @@ +--- +title: Add time stats to Issue and Merge Request API +merge_request: 13335 +author: @travismiller diff --git a/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml b/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml new file mode 100644 index 00000000000..a37de4325bb --- /dev/null +++ b/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml @@ -0,0 +1,5 @@ +--- +title: Remove unwanted refs after importing a project +merge_request: 13766 +author: +type: other diff --git a/changelogs/unreleased/check-trigger-permissions.yml b/changelogs/unreleased/check-trigger-permissions.yml new file mode 100644 index 00000000000..e0809cea9bf --- /dev/null +++ b/changelogs/unreleased/check-trigger-permissions.yml @@ -0,0 +1,5 @@ +--- +title: Improve migrations using triggers +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/replace_spinach_user_lookup-feature.yml b/changelogs/unreleased/replace_spinach_user_lookup-feature.yml new file mode 100644 index 00000000000..36248c54d99 --- /dev/null +++ b/changelogs/unreleased/replace_spinach_user_lookup-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace 'project/user_lookup.feature' spinach test with an rspec analog +merge_request: 13863 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/doc/api/issues.md b/doc/api/issues.md index f30ed08d0fa..14635114a31 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -101,6 +101,12 @@ Example response: "user_notes_count": 1, "due_date": "2016-07-22", "web_url": "http://example.com/example/example/issues/6", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false } ] @@ -198,6 +204,12 @@ Example response: "user_notes_count": 1, "due_date": null, "web_url": "http://example.com/example/example/issues/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false } ] @@ -296,6 +308,12 @@ Example response: "user_notes_count": 1, "due_date": "2016-07-22", "web_url": "http://example.com/example/example/issues/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false } ] @@ -372,6 +390,12 @@ Example response: "user_notes_count": 1, "due_date": null, "web_url": "http://example.com/example/example/issues/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", @@ -440,6 +464,12 @@ Example response: "user_notes_count": 0, "due_date": null, "web_url": "http://example.com/example/example/issues/14", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", @@ -509,6 +539,12 @@ Example response: "user_notes_count": 0, "due_date": "2016-07-22", "web_url": "http://example.com/example/example/issues/15", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", @@ -601,6 +637,12 @@ Example response: }, "due_date": null, "web_url": "http://example.com/example/example/issues/11", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", @@ -672,6 +714,12 @@ Example response: }, "due_date": null, "web_url": "http://example.com/example/example/issues/11", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + }, "confidential": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", @@ -1001,7 +1049,13 @@ Example response: "user_notes_count": 1, "should_remove_source_branch": null, "force_remove_source_branch": false, - "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/6432" + "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/6432", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ] ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 802e5362d70..4f67aa4b9d4 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -92,7 +92,13 @@ Parameters: "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, - "web_url": "http://example.com/example/example/merge_requests/1" + "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ] ``` @@ -181,7 +187,13 @@ Parameters: "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, - "web_url": "http://example.com/example/example/merge_requests/1" + "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ] ``` @@ -250,7 +262,13 @@ Parameters: "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, - "web_url": "http://example.com/example/example/merge_requests/1" + "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ``` @@ -356,6 +374,12 @@ Parameters: "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } "changes": [ { "old_path": "VERSION", @@ -442,7 +466,13 @@ POST /projects/:id/merge_requests "user_notes_count": 0, "should_remove_source_branch": true, "force_remove_source_branch": false, - "web_url": "http://example.com/example/example/merge_requests/1" + "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ``` @@ -519,7 +549,13 @@ Must include at least one non-required attribute from above. "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, - "web_url": "http://example.com/example/example/merge_requests/1" + "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ``` @@ -617,7 +653,13 @@ Parameters: "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, - "web_url": "http://example.com/example/example/merge_requests/1" + "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ``` @@ -687,7 +729,13 @@ Parameters: "user_notes_count": 1, "should_remove_source_branch": true, "force_remove_source_branch": false, - "web_url": "http://example.com/example/example/merge_requests/1" + "web_url": "http://example.com/example/example/merge_requests/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": null, + "human_total_time_spent": null + } } ``` diff --git a/doc/user/project/members/img/other_group_sees_shared_project.png b/doc/user/project/members/img/other_group_sees_shared_project.png Binary files differindex 67af27043eb..e4c93a13abb 100644 --- a/doc/user/project/members/img/other_group_sees_shared_project.png +++ b/doc/user/project/members/img/other_group_sees_shared_project.png diff --git a/doc/user/project/members/img/share_project_with_groups.png b/doc/user/project/members/img/share_project_with_groups.png Binary files differindex 3cb4796f9f7..0907438cb84 100644 --- a/doc/user/project/members/img/share_project_with_groups.png +++ b/doc/user/project/members/img/share_project_with_groups.png diff --git a/doc/user/project/members/img/share_project_with_groups_tab.png b/doc/user/project/members/img/share_project_with_groups_tab.png Binary files differnew file mode 100644 index 00000000000..fc489aae003 --- /dev/null +++ b/doc/user/project/members/img/share_project_with_groups_tab.png diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index 4c1ddcdcba8..25e5b897825 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -5,7 +5,7 @@ possible to add a group of users to a project with a single action. ## Groups as collections of users -Groups are used primarily to [create collections of projects](../user/group/index.md), but you can also +Groups are used primarily to [create collections of projects](../../group/index.md), but you can also take advantage of the fact that groups define collections of _users_, namely the group members. @@ -16,20 +16,23 @@ say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'P Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'? This is where the group sharing feature can be of use. -To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section. +To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the **Settings > Members** section. -![The 'Groups' section in the project settings screen](img/share_project_with_groups.png) +![share project with groups](img/share_project_with_groups.png) -Now you can add the 'Engineering' group with the maximum access level of your choice. -After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard. +Then select the 'Share with group' tab by clicking it. + +Now you can add the 'Engineering' group with the maximum access level of your choice. Click 'Share' to share it. + +![share project with groups tab](img/share_project_with_groups_tab.png) + +After sharing 'Project Acme' with 'Engineering', the project will be listed on the group dashboard. !['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project.png) ## Maximum access level -!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](img/max_access_level.png) - -In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'. +In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'. ## Share project with group lock (EES/EEP) diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index e853bfff444..4e93e680fd2 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -100,16 +100,21 @@ 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. +>**Note:** +The latest artifacts are considered as the artifacts created by jobs in the +latest pipeline that succeeded for the specific ref. +Artifacts for other pipelines can be accessed with direct access to them. + 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> +https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/download?job=<job_name> ``` To download a single file from the artifacts use the following URL: ``` -https://example.com/<namespace>/<project>/builds/artifacts/<ref>/raw/<path_to_file>?job=<job_name> +https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/raw/<path_to_file>?job=<job_name> ``` For example, to download the latest artifacts of the job named `coverage` of @@ -117,26 +122,26 @@ 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=coverage +https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/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/raw/coverage/index.html?job=coverage +https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/master/raw/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> +https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/browse?job=<job_name> ``` For example: ``` -https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/browse?job=coverage +https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/master/browse?job=coverage ``` The latest builds are also exposed in the UI in various places. Specifically, diff --git a/features/project/commits/user_lookup.feature b/features/project/commits/user_lookup.feature deleted file mode 100644 index c18f4e070f3..00000000000 --- a/features/project/commits/user_lookup.feature +++ /dev/null @@ -1,16 +0,0 @@ -@project_commits -Feature: Project Commits User Lookup - Background: - Given I sign in as a user - And I own a project - And I visit my project's commits page - - Scenario: I browse commit from list - Given I have user with primary email - When I click on commit link - Then I see author based on primary email - - Scenario: I browse another commit from list - Given I have user with secondary email - When I click on another commit link - Then I see author based on secondary email diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb deleted file mode 100644 index 4599e0d032a..00000000000 --- a/features/steps/project/commits/user_lookup.rb +++ /dev/null @@ -1,49 +0,0 @@ -class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I click on commit link' do - visit project_commit_path(@project, sample_commit.id) - end - - step 'I click on another commit link' do - visit project_commit_path(@project, sample_commit.parent_id) - end - - step 'I have user with primary email' do - user_primary - end - - step 'I have user with secondary email' do - user_secondary - end - - step 'I see author based on primary email' do - check_author_link(sample_commit.author_email, user_primary) - end - - step 'I see author based on secondary email' do - check_author_link(sample_commit.author_email, user_secondary) - end - - def check_author_link(email, user) - author_link = find('.commit-author-link') - - expect(author_link['href']).to eq user_path(user) - expect(author_link['title']).to eq email - expect(find('.commit-author-name').text).to eq user.name - end - - def user_primary - @user_primary ||= create(:user, email: 'dmitriy.zaporozhets@gmail.com') - end - - def user_secondary - @user_secondary ||= begin - user = create(:user, email: 'dzaporozhets@example.com') - create(:email, { user: user, email: 'dmitriy.zaporozhets@gmail.com' }) - user - end - end -end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index b87f7cdbad1..a989394ad91 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -130,7 +130,7 @@ module API commit = user_project.repository.commit(branch.dereferenced_target) - destroy_conditionally!(commit, last_update_field: :authored_date) do + destroy_conditionally!(commit, last_updated: commit.authored_date) do result = DeleteBranchService.new(user_project, current_user) .execute(params[:branch]) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index e8dd61e493f..803b48dd88a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -320,7 +320,10 @@ module API end class IssueBasic < ProjectEntity - expose :label_names, as: :labels + expose :labels do |issue, options| + # Avoids an N+1 query since labels are preloaded + issue.labels.map(&:title).sort + end expose :milestone, using: Entities::Milestone expose :assignees, :author, using: Entities::UserBasic @@ -329,13 +332,32 @@ module API end expose :user_notes_count - expose :upvotes, :downvotes + expose :upvotes do |issue, options| + if options[:issuable_metadata] + # Avoids an N+1 query when metadata is included + options[:issuable_metadata][issue.id].upvotes + else + issue.upvotes + end + end + expose :downvotes do |issue, options| + if options[:issuable_metadata] + # Avoids an N+1 query when metadata is included + options[:issuable_metadata][issue.id].downvotes + else + issue.downvotes + end + end expose :due_date expose :confidential expose :web_url do |issue, options| Gitlab::UrlBuilder.build(issue) end + + expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue| + issue + end end class Issue < IssueBasic @@ -365,10 +387,22 @@ module API end class IssuableTimeStats < Grape::Entity + format_with(:time_tracking_formatter) do |time_spent| + Gitlab::TimeTrackingFormatter.output(time_spent) + end + expose :time_estimate expose :total_time_spent expose :human_time_estimate - expose :human_total_time_spent + + with_options(format_with: :time_tracking_formatter) do + expose :total_time_spent, as: :human_total_time_spent + end + + def total_time_spent + # Avoids an N+1 query since timelogs are preloaded + object.timelogs.map(&:time_spent).sum + end end class ExternalIssue < Grape::Entity @@ -418,6 +452,10 @@ module API expose :web_url do |merge_request, options| Gitlab::UrlBuilder.build(merge_request) end + + expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request| + merge_request + end end class MergeRequest < MergeRequestBasic diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 84980864151..3d377fdb9eb 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -19,8 +19,10 @@ module API end end - def destroy_conditionally!(resource, last_update_field: :updated_at) - check_unmodified_since!(resource.public_send(last_update_field)) + def destroy_conditionally!(resource, last_updated: nil) + last_updated ||= resource.updated_at + + check_unmodified_since!(last_updated) status 204 if block_given? diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6503629e2a2..0297023226f 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -4,6 +4,8 @@ module API before { authenticate! } + helpers ::Gitlab::IssuableMetadata + helpers do def find_issues(args = {}) args = params.merge(args) @@ -13,6 +15,7 @@ module API args[:label_name] = args.delete(:labels) issues = IssuesFinder.new(current_user, args).execute + .preload(:assignees, :labels, :notes, :timelogs) issues.reorder(args[:order_by] => args[:sort]) end @@ -65,7 +68,13 @@ module API get do issues = find_issues - present paginate(issues), with: Entities::IssueBasic, current_user: current_user + options = { + with: Entities::IssueBasic, + current_user: current_user, + issuable_metadata: issuable_meta_data(issues, 'Issue') + } + + present paginate(issues), options end end @@ -86,7 +95,13 @@ module API issues = find_issues(group_id: group.id) - present paginate(issues), with: Entities::IssueBasic, current_user: current_user + options = { + with: Entities::IssueBasic, + current_user: current_user, + issuable_metadata: issuable_meta_data(issues, 'Issue') + } + + present paginate(issues), options end end @@ -109,7 +124,14 @@ module API issues = find_issues(project_id: project.id) - present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project + options = { + with: Entities::IssueBasic, + current_user: current_user, + project: user_project, + issuable_metadata: issuable_meta_data(issues, 'Issue') + } + + present paginate(issues), options end desc 'Get a single project issue' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 969c6064662..eec8d9357aa 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -21,7 +21,7 @@ module API return merge_requests if args[:view] == 'simple' merge_requests - .preload(:notes, :author, :assignee, :milestone, :merge_request_diff, :labels) + .preload(:notes, :author, :assignee, :milestone, :merge_request_diff, :labels, :timelogs) end params :merge_requests_params do diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 81b17935b81..912415e3a7f 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -70,7 +70,7 @@ module API commit = user_project.repository.commit(tag.dereferenced_target) - destroy_conditionally!(commit, last_update_field: :authored_date) do + destroy_conditionally!(commit, last_updated: commit.authored_date) do result = ::Tags::DestroyService.new(user_project, current_user) .execute(params[:tag_name]) diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index e001d25e7b7..a6ec75da385 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -9,6 +9,14 @@ module Gitlab ActiveRecord::Base.configurations[Rails.env] end + def self.username + config['username'] || ENV['USER'] + end + + def self.database_name + config['database'] + end + def self.adapter_name config['adapter'] end diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb new file mode 100644 index 00000000000..aee3981e79a --- /dev/null +++ b/lib/gitlab/database/grant.rb @@ -0,0 +1,34 @@ +module Gitlab + module Database + # Model that can be used for querying permissions of a SQL user. + class Grant < ActiveRecord::Base + self.table_name = + if Database.postgresql? + 'information_schema.role_table_grants' + else + 'mysql.user' + end + + def self.scope_to_current_user + if Database.postgresql? + where('grantee = user') + else + where("CONCAT(User, '@', Host) = current_user()") + end + end + + # Returns true if the current user can create and execute triggers on the + # given table. + def self.create_and_execute_trigger?(table) + priv = + if Database.postgresql? + where(privilege_type: 'TRIGGER', table_name: table) + else + where(Trigger_priv: 'Y') + end + + priv.scope_to_current_user.any? + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 5e2c6cc5cad..fb14798efe6 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -358,6 +358,8 @@ module Gitlab raise 'rename_column_concurrently can not be run inside a transaction' end + check_trigger_permissions!(table) + old_col = column_for(table, old) new_type = type || old_col.type @@ -430,6 +432,8 @@ module Gitlab def cleanup_concurrent_column_rename(table, old, new) trigger_name = rename_trigger_name(table, old, new) + check_trigger_permissions!(table) + if Database.postgresql? remove_rename_triggers_for_postgresql(table, trigger_name) else @@ -485,14 +489,14 @@ module Gitlab # Removes the triggers used for renaming a PostgreSQL column concurrently. def remove_rename_triggers_for_postgresql(table, trigger) - execute("DROP TRIGGER #{trigger} ON #{table}") - execute("DROP FUNCTION #{trigger}()") + execute("DROP TRIGGER IF EXISTS #{trigger} ON #{table}") + execute("DROP FUNCTION IF EXISTS #{trigger}()") end # Removes the triggers used for renaming a MySQL column concurrently. def remove_rename_triggers_for_mysql(trigger) - execute("DROP TRIGGER #{trigger}_insert") - execute("DROP TRIGGER #{trigger}_update") + execute("DROP TRIGGER IF EXISTS #{trigger}_insert") + execute("DROP TRIGGER IF EXISTS #{trigger}_update") end # Returns the (base) name to use for triggers when renaming columns. @@ -625,6 +629,30 @@ module Gitlab conn.llen("queue:#{queue_name}") end end + + def check_trigger_permissions!(table) + unless Grant.create_and_execute_trigger?(table) + dbname = Database.database_name + user = Database.username + + raise <<-EOF +Your database user is not allowed to create, drop, or execute triggers on the +table #{table}. + +If you are using PostgreSQL you can solve this by logging in to the GitLab +database (#{dbname}) using a super user and running: + + ALTER #{user} WITH SUPERUSER + +For MySQL you instead need to run: + + GRANT ALL PRIVILEGES ON *.* TO #{user}@'%' + +Both queries will grant the user super user permissions, ensuring you don't run +into similar problems in the future (e.g. when new tables are created). + EOF + end + end end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 03e2bec84dd..fb6504bdea0 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -17,6 +17,7 @@ module Gitlab NoRepository = Class.new(StandardError) InvalidBlobName = Class.new(StandardError) InvalidRef = Class.new(StandardError) + GitError = Class.new(StandardError) class << self # Unlike `new`, `create` takes the storage path, not the storage name @@ -246,6 +247,13 @@ module Gitlab branch_names + tag_names end + # Returns an Array of all ref names, except when it's matching pattern + # + # regexp - The pattern for ref names we don't want + def all_ref_names_except(regexp) + rugged.references.reject { |ref| ref.name =~ regexp }.map(&:name) + end + # Discovers the default branch based on the repository's available branches # # - If no branches are present, returns nil @@ -591,6 +599,23 @@ module Gitlab rugged.branches.delete(branch_name) end + def delete_refs(*ref_names) + instructions = ref_names.map do |ref| + "delete #{ref}\x00\x00" + end + + command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] + message, status = Gitlab::Popen.popen( + command, + path) do |stdin| + stdin.write(instructions.join) + end + + unless status.zero? + raise GitError.new("Could not delete refs #{ref_names}: #{message}") + end + end + # Create a new branch named **ref+ based on **stat_point+, HEAD by default # # Examples: diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index f76bef5f4bf..8ae1b6a626a 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -111,7 +111,7 @@ namespace :gitlab do next unless id > max_iid project.deployments.find(id).create_ref - rugged.references.delete(ref) + project.repository.delete_refs(ref) end end end diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb index db958346f06..db958346f06 100644 --- a/spec/features/projects/commit/rss_spec.rb +++ b/spec/features/projects/commits/rss_spec.rb diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb new file mode 100644 index 00000000000..41f3c15a94c --- /dev/null +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'User broweses commits' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, namespace: user.namespace) } + + before do + project.add_master(user) + sign_in(user) + end + + context 'primary email' do + it 'finds a commit by a primary email' do + user = create(:user, email: 'dmitriy.zaporozhets@gmail.com') + + visit(project_commit_path(project, RepoHelpers.sample_commit.id)) + + check_author_link(RepoHelpers.sample_commit.author_email, user) + end + end + + context 'secondary email' do + it 'finds a commit by a secondary email' do + user = + create(:user) do |user| + create(:email, { user: user, email: 'dmitriy.zaporozhets@gmail.com' }) + end + + visit(project_commit_path(project, RepoHelpers.sample_commit.parent_id)) + + check_author_link(RepoHelpers.sample_commit.author_email, user) + end + end +end + +private + +def check_author_link(email, author) + author_link = find('.commit-author-link') + + expect(author_link['href']).to eq(user_path(author)) + expect(author_link['title']).to eq(email) + expect(find('.commit-author-name').text).to eq(author.name) +end diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json index bd6bfc03199..8acd9488215 100644 --- a/spec/fixtures/api/schemas/public_api/v4/issues.json +++ b/spec/fixtures/api/schemas/public_api/v4/issues.json @@ -78,7 +78,13 @@ "downvotes": { "type": "integer" }, "due_date": { "type": ["date", "null"] }, "confidential": { "type": "boolean" }, - "web_url": { "type": "uri" } + "web_url": { "type": "uri" }, + "time_stats": { + "time_estimate": { "type": "integer" }, + "total_time_spent": { "type": "integer" }, + "human_time_estimate": { "type": ["string", "null"] }, + "human_total_time_spent": { "type": ["string", "null"] } + } }, "required": [ "id", "iid", "project_id", "title", "description", diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index 60aa47c1259..31b3f4ba946 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -72,7 +72,13 @@ "user_notes_count": { "type": "integer" }, "should_remove_source_branch": { "type": ["boolean", "null"] }, "force_remove_source_branch": { "type": ["boolean", "null"] }, - "web_url": { "type": "uri" } + "web_url": { "type": "uri" }, + "time_stats": { + "time_estimate": { "type": "integer" }, + "total_time_spent": { "type": "integer" }, + "human_time_estimate": { "type": ["string", "null"] }, + "human_total_time_spent": { "type": ["string", "null"] } + } }, "required": [ "id", "iid", "project_id", "title", "description", diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/javascripts/issue_show/components/edited_spec.js index a0d0750ae34..2061def699b 100644 --- a/spec/javascripts/issue_show/components/edited_spec.js +++ b/spec/javascripts/issue_show/components/edited_spec.js @@ -46,4 +46,14 @@ describe('edited', () => { expect(editedComponent.$el.querySelector('.author_link')).toBeFalsy(); expect(editedComponent.$el.querySelector('time')).toBeTruthy(); }); + + it('renders time ago tooltip at the bottom', () => { + const editedComponent = new EditedComponent({ + propsData: { + updatedAt: '2017-05-15T12:31:04.428Z', + }, + }).$mount(); + + expect(editedComponent.$el.querySelector('time').dataset.placement).toEqual('bottom'); + }); }); diff --git a/spec/lib/gitlab/database/grant_spec.rb b/spec/lib/gitlab/database/grant_spec.rb new file mode 100644 index 00000000000..651da3e8476 --- /dev/null +++ b/spec/lib/gitlab/database/grant_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Database::Grant do + describe '.scope_to_current_user' do + it 'scopes the relation to the current user' do + user = Gitlab::Database.username + column = Gitlab::Database.postgresql? ? :grantee : :User + names = described_class.scope_to_current_user.pluck(column).uniq + + expect(names).to eq([user]) + end + end + + describe '.create_and_execute_trigger' do + it 'returns true when the user can create and execute a trigger' do + # We assume the DB/user is set up correctly so that triggers can be + # created, which is necessary anyway for other tests to work. + expect(described_class.create_and_execute_trigger?('users')).to eq(true) + end + + it 'returns false when the user can not create and/or execute a trigger' do + allow(described_class).to receive(:scope_to_current_user) + .and_return(described_class.none) + + result = described_class.create_and_execute_trigger?('kittens') + + expect(result).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index c25fd459dd7..1bcdc369c44 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -450,6 +450,8 @@ describe Gitlab::Database::MigrationHelpers do it 'renames a column concurrently' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:install_rename_triggers_for_mysql) .with(trigger_name, 'users', 'old', 'new') @@ -477,6 +479,8 @@ describe Gitlab::Database::MigrationHelpers do it 'renames a column concurrently' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:install_rename_triggers_for_postgresql) .with(trigger_name, 'users', 'old', 'new') @@ -506,6 +510,8 @@ describe Gitlab::Database::MigrationHelpers do it 'cleans up the renaming procedure for PostgreSQL' do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:remove_rename_triggers_for_postgresql) .with(:users, /trigger_.{12}/) @@ -517,6 +523,8 @@ describe Gitlab::Database::MigrationHelpers do it 'cleans up the renaming procedure for MySQL' do allow(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(model).to receive(:check_trigger_permissions!).with(:users) + expect(model).to receive(:remove_rename_triggers_for_mysql) .with(/trigger_.{12}/) @@ -573,8 +581,8 @@ describe Gitlab::Database::MigrationHelpers do describe '#remove_rename_triggers_for_postgresql' do it 'removes the function and trigger' do - expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar') - expect(model).to receive(:execute).with('DROP FUNCTION foo()') + expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo ON bar') + expect(model).to receive(:execute).with('DROP FUNCTION IF EXISTS foo()') model.remove_rename_triggers_for_postgresql('bar', 'foo') end @@ -582,8 +590,8 @@ describe Gitlab::Database::MigrationHelpers do describe '#remove_rename_triggers_for_mysql' do it 'removes the triggers' do - expect(model).to receive(:execute).with('DROP TRIGGER foo_insert') - expect(model).to receive(:execute).with('DROP TRIGGER foo_update') + expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo_insert') + expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo_update') model.remove_rename_triggers_for_mysql('foo') end @@ -890,4 +898,20 @@ describe Gitlab::Database::MigrationHelpers do end end end + + describe '#check_trigger_permissions!' do + it 'does nothing when the user has the correct permissions' do + expect { model.check_trigger_permissions!('users') } + .not_to raise_error(RuntimeError) + end + + it 'raises RuntimeError when the user does not have the correct permissions' do + allow(Gitlab::Database::Grant).to receive(:create_and_execute_trigger?) + .with('kittens') + .and_return(false) + + expect { model.check_trigger_permissions!('kittens') } + .to raise_error(RuntimeError, /Your database user is not allowed/) + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 6b9773c9b63..4cfb4b7d357 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -433,6 +433,40 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#delete_refs' do + before(:all) do + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + + it 'deletes the ref' do + @repo.delete_refs('refs/heads/feature') + + expect(@repo.rugged.references['refs/heads/feature']).to be_nil + end + + it 'deletes all refs' do + refs = %w[refs/heads/wip refs/tags/v1.1.0] + @repo.delete_refs(*refs) + + refs.each do |ref| + expect(@repo.rugged.references[ref]).to be_nil + end + end + + it 'raises an error if it failed' do + expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1]) + + expect do + @repo.delete_refs('refs/heads/fix') + end.to raise_error(Gitlab::Git::Repository::GitError) + end + + after(:all) do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + end + describe "#refs_hash" do let(:refs) { repository.refs_hash } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2e613c44357..11717ba39e8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1563,10 +1563,18 @@ describe Project do describe 'project import state transitions' do context 'state transition: [:started] => [:finished]' do - let(:housekeeping_service) { spy } + let(:after_import_service) { spy(:after_import_service) } + let(:housekeeping_service) { spy(:housekeeping_service) } before do - allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service } + allow(Projects::AfterImportService) + .to receive(:new) { after_import_service } + + allow(after_import_service) + .to receive(:execute) { housekeeping_service.execute } + + allow(Projects::HousekeepingService) + .to receive(:new) { housekeeping_service } end it 'resets project import_error' do @@ -1581,6 +1589,7 @@ describe Project do project.import_finish + expect(after_import_service).to have_received(:execute) expect(housekeeping_service).to have_received(:execute) end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9a0c62467d3..dee75c96b86 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -509,6 +509,18 @@ describe API::Issues, :mailer do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + it 'avoids N+1 queries' do + control_count = ActiveRecord::QueryRecorder.new do + get api("/projects/#{project.id}/issues", user) + end.count + + create(:issue, author: user, project: project) + + expect do + get api("/projects/#{project.id}/issues", user) + end.not_to exceed_query_limit(control_count) + end + it 'returns 404 when project does not exist' do get api('/projects/1000/issues', non_member) diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb new file mode 100644 index 00000000000..c6678fc1f5c --- /dev/null +++ b/spec/services/projects/after_import_service_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Projects::AfterImportService do + subject { described_class.new(project) } + + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:sha) { project.commit.sha } + let(:housekeeping_service) { double(:housekeeping_service) } + + describe '#execute' do + before do + allow(Projects::HousekeepingService) + .to receive(:new).with(project).and_return(housekeeping_service) + + allow(housekeeping_service) + .to receive(:execute).and_yield + end + + it 'performs housekeeping' do + subject.execute + + expect(housekeeping_service).to have_received(:execute) + end + + context 'with some refs in refs/pull/**/*' do + before do + repository.write_ref('refs/pull/1/head', sha) + repository.write_ref('refs/pull/1/merge', sha) + + subject.execute + end + + it 'removes refs/pull/**/*' do + expect(repository.rugged.references.map(&:name)) + .not_to include(%r{\Arefs/pull/}) + end + end + + Repository::RESERVED_REFS_NAMES.each do |name| + context "with a ref in refs/#{name}/tmp" do + before do + repository.write_ref("refs/#{name}/tmp", sha) + + subject.execute + end + + it "does not remove refs/#{name}/tmp" do + expect(repository.rugged.references.map(&:name)) + .to include("refs/#{name}/tmp") + end + end + end + end +end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 385f56e447f..9386c110385 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -23,6 +23,12 @@ describe Projects::HousekeepingService do expect(project.reload.pushes_since_gc).to eq(0) end + it 'yields the block if given' do + expect do |block| + subject.execute(&block) + end.to yield_with_no_args + end + context 'when no lease can be obtained' do before do expect(subject).to receive(:try_obtain_lease).and_return(false) @@ -39,6 +45,13 @@ describe Projects::HousekeepingService do expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) end.not_to change { project.pushes_since_gc } end + + it 'does not yield' do + expect do |block| + expect { subject.execute(&block) } + .to raise_error(Projects::HousekeepingService::LeaseTaken) + end.not_to yield_with_no_args + end end end |