diff options
33 files changed, 468 insertions, 162 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f038ce72aeb..80ba8e5c1a1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,6 +61,9 @@ stages: .use-pg: &use-pg services: + # As of Jan 2018, we don't have a strong reason to upgrade to 9.6 for CI yet, + # so using the least common denominator ensures backwards compatibility + # (as many users are still using 9.2). - postgres:9.2 - redis:alpine diff --git a/Gemfile.lock b/Gemfile.lock index cb6b0ebb3bc..8e31ac1f993 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -340,6 +340,8 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) google-protobuf (3.4.1.1) + googleapis-common-protos-types (1.0.1) + google-protobuf (~> 3.0) googleauth (0.5.3) faraday (~> 0.12) jwt (~> 1.4) @@ -366,9 +368,10 @@ GEM rake grape_logging (1.7.0) grape - grpc (1.4.5) + grpc (1.8.3) google-protobuf (~> 3.1) - googleauth (~> 0.5.1) + googleapis-common-protos-types (~> 1.0.0) + googleauth (>= 0.5.1, < 0.7) haml (4.0.7) tilt haml_lint (0.26.0) diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 25cef44c1b8..ff2e0768a87 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -46,14 +46,15 @@ )); const extraCostParagraph = sprintf( - _.escape(s__(`ClusterIntegration|%{boldNotice} This will add some -extra resources like a load balancer, -which incur additional costs. See %{pricingLink}`)), - { + _.escape(s__( + `ClusterIntegration|%{boldNotice} This will add some extra resources + like a load balancer, which may incur additional costs depending on + the hosting provider Kubernetes is installed on. If you are using GKE, + you can %{pricingLink}.`, + )), { boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|GKE pricing'))} - </a>`, + ${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`, }, false, ); @@ -80,8 +81,7 @@ which incur additional costs. See %{pricingLink}`)), { gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|Gitlab Integration'))} - </a>`, + ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, }, false, ); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 72a6426e901..6880784d7d0 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -30,13 +30,10 @@ import CommitsList from './commits'; import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import SecretValues from './behaviors/secret_values'; -import DeleteModal from './branches/branches_delete_modal'; import Group from './group'; import ProjectsList from './projects_list'; import setupProjectEdit from './project_edit'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; -import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; -import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; import ShortcutsWiki from './shortcuts_wiki'; import BlobViewer from './blob/viewer/index'; @@ -44,7 +41,6 @@ import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import UsersSelect from './users_select'; import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; -import ShortcutsBlob from './shortcuts_blob'; import Star from './star'; import TreeView from './tree'; import Wikis from './wikis'; @@ -52,14 +48,12 @@ import ZenMode from './zen_mode'; import initSettingsPanels from './settings_panels'; import PerformanceBar from './performance_bar'; import initNotes from './init_notes'; -import initLegacyFilters from './init_legacy_filters'; import initIssuableSidebar from './init_issuable_sidebar'; import initProjectVisibilitySelector from './project_visibility'; import GpgBadges from './gpg_badges'; import initChangesDropdown from './init_changes_dropdown'; import NewGroupChild from './groups/new_group_child'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; -import AjaxLoadingSpinner from './ajax_loading_spinner'; import GlFieldErrors from './gl_field_errors'; import GLForm from './gl_form'; import Shortcuts from './shortcuts'; @@ -86,7 +80,7 @@ import Activities from './activities'; } Dispatcher.prototype.initPageScripts = function() { - var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; + var path, shortcut_handler; const page = $('body').attr('data-page'); if (!page) { return false; @@ -111,33 +105,6 @@ import Activities from './activities'; }); }); - function initBlob() { - new LineHighlighter(); - - new BlobLinePermalinkUpdater( - document.querySelector('#blob-content-holder'), - '.diff-line-num[data-line-number]', - document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), - ); - - shortcut_handler = new ShortcutsNavigation(); - fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); - new ShortcutsBlob({ - skipResetBindings: true, - fileBlobPermalinkUrl, - }); - - new BlobForkSuggestion({ - openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'), - forkButtons: document.querySelectorAll('.js-fork-suggestion-button'), - cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), - suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), - actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), - }) - .init(); - } - const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); switch (page) { @@ -148,8 +115,10 @@ import Activities from './activities'; break; case 'projects:boards:show': case 'projects:boards:index': - shortcut_handler = new ShortcutsNavigation(); - new UsersSelect(); + import('./pages/projects/boards/index') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:merge_requests:index': case 'projects:issues:index': @@ -190,8 +159,9 @@ import Activities from './activities'; .catch(fail); break; case 'dashboard:merge_requests': - projectSelect(); - initLegacyFilters(); + import('./pages/dashboard/merge_requests') + .then(callDefault) + .catch(fail); break; case 'groups:issues': case 'groups:merge_requests': @@ -246,8 +216,9 @@ import Activities from './activities'; new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); break; case 'projects:branches:index': - AjaxLoadingSpinner.init(); - new DeleteModal(); + import('./pages/projects/branches/index') + .then(callDefault) + .catch(fail); break; case 'projects:issues:new': case 'projects:issues:edit': @@ -458,11 +429,16 @@ import Activities from './activities'; shortcut_handler = true; break; case 'projects:blob:show': - new BlobViewer(); - initBlob(); + import('./pages/projects/blob/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:blame:show': - initBlob(); + import('./pages/projects/blame/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'groups:labels:new': case 'groups:labels:edit': diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 1f18c196137..3c8452ac808 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -271,7 +271,7 @@ Please check your network connection and try again.`; <div class="timeline-content timeline-content-form"> <form ref="commentForm" - class="new-note js-quick-submit common-note-form gfm-form js-main-target-form" + class="new-note common-note-form gfm-form js-main-target-form" > <div class="error-alert"></div> @@ -301,7 +301,8 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" :disabled="isSubmitting" placeholder="Write a comment or drag your files here..." @keydown.up="editCurrentUserLastNote()" - @keydown.meta.enter="handleSave()"> + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()"> </textarea> </markdown-field> <div class="note-form-actions"> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index aeda3497715..d382a9bb642 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -155,6 +155,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" slot="textarea" placeholder="Write a comment or drag your files here..." @keydown.meta.enter="handleUpdate()" + @keydown.ctrl.enter="handleUpdate()" @keydown.up="editMyLastNote()" @keydown.esc="cancelHandler(true)"> </textarea> diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js new file mode 100644 index 00000000000..480357a309c --- /dev/null +++ b/app/assets/javascripts/pages/projects/blame/show/index.js @@ -0,0 +1,3 @@ +import initBlob from '~/pages/projects/init_blob'; + +export default initBlob; diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js new file mode 100644 index 00000000000..a3eeb1cefb6 --- /dev/null +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -0,0 +1,7 @@ +import BlobViewer from '~/blob/viewer/index'; +import initBlob from '~/pages/projects/init_blob'; + +export default () => { + new BlobViewer(); // eslint-disable-line no-new + initBlob(); +}; diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js new file mode 100644 index 00000000000..42c9bb5ec99 --- /dev/null +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -0,0 +1,7 @@ +import UsersSelect from '~/users_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default () => { + new UsersSelect(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js new file mode 100644 index 00000000000..cee0f19bf2a --- /dev/null +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -0,0 +1,7 @@ +import AjaxLoadingSpinner from '~/ajax_loading_spinner'; +import DeleteModal from '~/branches/branches_delete_modal'; + +export default () => { + AjaxLoadingSpinner.init(); + new DeleteModal(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js new file mode 100644 index 00000000000..26f0ad46114 --- /dev/null +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -0,0 +1,33 @@ +import LineHighlighter from '~/line_highlighter'; +import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsBlob from '~/shortcuts_blob'; +import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; + +export default () => { + new LineHighlighter(); // eslint-disable-line no-new + + new BlobLinePermalinkUpdater( // eslint-disable-line no-new + document.querySelector('#blob-content-holder'), + '.diff-line-num[data-line-number]', + document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), + ); + + const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); + const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + + new ShortcutsNavigation(); // eslint-disable-line no-new + + new ShortcutsBlob({ // eslint-disable-line no-new + skipResetBindings: true, + fileBlobPermalinkUrl, + }); + + new BlobForkSuggestion({ // eslint-disable-line no-new + openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'), + forkButtons: document.querySelectorAll('.js-fork-suggestion-button'), + cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), + suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), + actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), + }).init(); +}; diff --git a/app/models/route.rb b/app/models/route.rb index 7ba3ec06041..412f5fb45a5 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -8,7 +8,7 @@ class Route < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validate :ensure_permanent_paths + validate :ensure_permanent_paths, if: :path_changed? after_create :delete_conflicting_redirects after_update :delete_conflicting_redirects, if: :path_changed? diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 42941acc508..3e85535dae0 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -7,7 +7,7 @@ .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do + = link_to params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do = icon('rss') = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index c5b1897c492..e759c87bda7 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -30,7 +30,7 @@ %li CI variables %li Any encrypted tokens %p - Once the exported file is ready, you will receive a notification email with a download link. + Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page. - if project.export_project_path = link_to 'Download export', download_export_project_path(project), rel: 'nofollow', download: '', method: :get, class: "btn btn-default" diff --git a/app/workers/concerns/project_import_options.rb b/app/workers/concerns/project_import_options.rb index 10b971344f7..ef23990ad97 100644 --- a/app/workers/concerns/project_import_options.rb +++ b/app/workers/concerns/project_import_options.rb @@ -1,9 +1,9 @@ module ProjectImportOptions extend ActiveSupport::Concern - included do - IMPORT_RETRY_COUNT = 5 + IMPORT_RETRY_COUNT = 5 + included do sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION # We only want to mark the project as failed once we exhausted all retries diff --git a/changelogs/unreleased/41163-improve-cluster-ingress-extra-cost-language.yml b/changelogs/unreleased/41163-improve-cluster-ingress-extra-cost-language.yml new file mode 100644 index 00000000000..9c48831855c --- /dev/null +++ b/changelogs/unreleased/41163-improve-cluster-ingress-extra-cost-language.yml @@ -0,0 +1,5 @@ +--- +title: Improve wording about additional costs for Ingress on custom clusters +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/41749-postgres-9-6-for-ci-tests.yml b/changelogs/unreleased/41749-postgres-9-6-for-ci-tests.yml new file mode 100644 index 00000000000..2a3d00f8e5f --- /dev/null +++ b/changelogs/unreleased/41749-postgres-9-6-for-ci-tests.yml @@ -0,0 +1,5 @@ +--- +title: Add reason to keep postgresql 9.2 for CI +merge_request: 16277 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/41882-respect-only-path-in-relative-link-filter.yml b/changelogs/unreleased/41882-respect-only-path-in-relative-link-filter.yml new file mode 100644 index 00000000000..d4b7ec6a3b5 --- /dev/null +++ b/changelogs/unreleased/41882-respect-only-path-in-relative-link-filter.yml @@ -0,0 +1,5 @@ +--- +title: Ensure that emails contain absolute, rather than relative, links to user uploads +merge_request: 16364 +author: +type: fixed diff --git a/changelogs/unreleased/41956-fix-ctrl-enter-binding-to-save-comment.yml b/changelogs/unreleased/41956-fix-ctrl-enter-binding-to-save-comment.yml new file mode 100644 index 00000000000..32a6f87d98e --- /dev/null +++ b/changelogs/unreleased/41956-fix-ctrl-enter-binding-to-save-comment.yml @@ -0,0 +1,5 @@ +--- +title: Fix Ctrl+Enter keyboard shortcut saving comment/note edit +merge_request: 16415 +author: +type: fixed diff --git a/changelogs/unreleased/mk-fix-permanent-redirect-validation.yml b/changelogs/unreleased/mk-fix-permanent-redirect-validation.yml new file mode 100644 index 00000000000..153b2ccc25c --- /dev/null +++ b/changelogs/unreleased/mk-fix-permanent-redirect-validation.yml @@ -0,0 +1,5 @@ +--- +title: Prevent invalid Route path if path is unchanged +merge_request: 16397 +author: +type: fixed diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 5c197afd782..f6169b2c85d 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -50,15 +50,22 @@ module Banzai end def process_link_to_upload_attr(html_attr) - uri_parts = [html_attr.value] + path_parts = [html_attr.value] if group - uri_parts.unshift(relative_url_root, 'groups', group.full_path, '-') + path_parts.unshift(relative_url_root, 'groups', group.full_path, '-') elsif project - uri_parts.unshift(relative_url_root, project.full_path) + path_parts.unshift(relative_url_root, project.full_path) end - html_attr.value = File.join(*uri_parts) + path = File.join(*path_parts) + + html_attr.value = + if context[:only_path] + path + else + URI.join(Gitlab.config.gitlab.base_url, path).to_s + end end def process_link_to_repository_attr(html_attr) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 6ada9a145db..d0467bca992 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1236,33 +1236,31 @@ module Gitlab end def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) - rebase_path = worktree_path(REBASE_WORKTREE_PREFIX, rebase_id) - env = git_env_for_user(user) - - if remote_repository.is_a?(RemoteRepository) - env.merge!(remote_repository.fetch_env) - remote_repo_path = GITALY_INTERNAL_URL - else - remote_repo_path = remote_repository.path - end - - with_worktree(rebase_path, branch, env: env) do - run_git!( - %W(pull --rebase #{remote_repo_path} #{remote_branch}), - chdir: rebase_path, env: env - ) - - rebase_sha = run_git!(%w(rev-parse HEAD), chdir: rebase_path, env: env).strip - - Gitlab::Git::OperationService.new(user, self) - .update_branch(branch, rebase_sha, branch_sha) - - rebase_sha + gitaly_migrate(:rebase) do |is_enabled| + if is_enabled + gitaly_rebase(user, rebase_id, + branch: branch, + branch_sha: branch_sha, + remote_repository: remote_repository, + remote_branch: remote_branch) + else + git_rebase(user, rebase_id, + branch: branch, + branch_sha: branch_sha, + remote_repository: remote_repository, + remote_branch: remote_branch) + end end end def rebase_in_progress?(rebase_id) - fresh_worktree?(worktree_path(REBASE_WORKTREE_PREFIX, rebase_id)) + gitaly_migrate(:rebase_in_progress) do |is_enabled| + if is_enabled + gitaly_repository_client.rebase_in_progress?(rebase_id) + else + fresh_worktree?(worktree_path(REBASE_WORKTREE_PREFIX, rebase_id)) + end + end end def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:) @@ -2039,6 +2037,40 @@ module Gitlab tree_id end + def gitaly_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) + gitaly_operation_client.user_rebase(user, rebase_id, + branch: branch, + branch_sha: branch_sha, + remote_repository: remote_repository, + remote_branch: remote_branch) + end + + def git_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) + rebase_path = worktree_path(REBASE_WORKTREE_PREFIX, rebase_id) + env = git_env_for_user(user) + + if remote_repository.is_a?(RemoteRepository) + env.merge!(remote_repository.fetch_env) + remote_repo_path = GITALY_INTERNAL_URL + else + remote_repo_path = remote_repository.path + end + + with_worktree(rebase_path, branch, env: env) do + run_git!( + %W(pull --rebase #{remote_repo_path} #{remote_branch}), + chdir: rebase_path, env: env + ) + + rebase_sha = run_git!(%w(rev-parse HEAD), chdir: rebase_path, env: env).strip + + Gitlab::Git::OperationService.new(user, self) + .update_branch(branch, rebase_sha, branch_sha) + + rebase_sha + end + end + def local_fetch_ref(source_path, source_ref:, target_ref:) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) run_git(args) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 37ba1af8d6f..7319de69d13 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -147,6 +147,34 @@ module Gitlab start_repository: start_repository) end + def user_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) + request = Gitaly::UserRebaseRequest.new( + repository: @gitaly_repo, + user: Gitlab::Git::User.from_gitlab(user).to_gitaly, + rebase_id: rebase_id.to_s, + branch: encode_binary(branch), + branch_sha: branch_sha, + remote_repository: remote_repository.gitaly_repository, + remote_branch: encode_binary(remote_branch) + ) + + response = GitalyClient.call( + @repository.storage, + :operation_service, + :user_rebase, + request, + remote_storage: remote_repository.storage + ) + + if response.pre_receive_error.presence + raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error + elsif response.git_error.presence + raise Gitlab::Git::Repository::GitError, response.git_error + else + response.rebase_sha + end + end + private def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 66006f5dc5b..72ee92e78dc 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -100,6 +100,23 @@ module Gitlab ) end + def rebase_in_progress?(rebase_id) + request = Gitaly::IsRebaseInProgressRequest.new( + repository: @gitaly_repo, + rebase_id: rebase_id.to_s + ) + + response = GitalyClient.call( + @storage, + :repository_service, + :is_rebase_in_progress, + request, + timeout: GitalyClient.default_timeout + ) + + response.in_progress + end + def fetch_source_branch(source_repository, source_branch, local_ref) request = Gitaly::FetchSourceBranchRequest.new( repository: @gitaly_repo, diff --git a/qa/README.md b/qa/README.md index 8fa04e80825..3c1b61900d9 100644 --- a/qa/README.md +++ b/qa/README.md @@ -17,6 +17,17 @@ against any existing instance. 1. Along with GitLab Docker Images we also build and publish GitLab QA images. 1. GitLab QA project uses these images to execute integration tests. +## Validating GitLab views / partials / selectors in merge requests + +We recently added a new CI job that is going to be triggered for every push +event in CE and EE projects. The job is called `qa:selectors` and it will +verify coupling between page objects implemented as a part of GitLab QA +and corresponding views / partials / selectors in CE / EE. + +Whenever `qa:selectors` job fails in your merge request, you are supposed to +fix [page objects](qa/page/README.md). You should also trigger end-to-end tests +using `package-qa` manual action, to test if everything works fine. + ## How can I use it? You can use GitLab QA to exercise tests on any live instance! For example, the diff --git a/spec/factories/redirect_routes.rb b/spec/factories/redirect_routes.rb new file mode 100644 index 00000000000..c29c81c5df9 --- /dev/null +++ b/spec/factories/redirect_routes.rb @@ -0,0 +1,15 @@ +FactoryBot.define do + factory :redirect_route do + sequence(:path) { |n| "redirect#{n}" } + source factory: :group + permanent false + + trait :permanent do + permanent true + end + + trait :temporary do + permanent false + end + end +end diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 20e352dd8bd..104d03377b6 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -139,13 +139,21 @@ describe('issue_comment_form component', () => { }); describe('event enter', () => { - it('should save note when cmd/ctrl+enter is pressed', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleSave').and.callThrough(); vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleSave).toHaveBeenCalled(); }); + + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleSave').and.callThrough(); + vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo'; + vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleSave).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 86e9e2a32a9..f841a408d09 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -69,13 +69,20 @@ describe('issue_note_form component', () => { }); describe('enter', () => { - it('should submit note', () => { + it('should save note when cmd+enter is pressed', () => { spyOn(vm, 'handleUpdate').and.callThrough(); vm.$el.querySelector('textarea').value = 'Foo'; vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true)); expect(vm.handleUpdate).toHaveBeenCalled(); }); + it('should save note when ctrl+enter is pressed', () => { + spyOn(vm, 'handleUpdate').and.callThrough(); + vm.$el.querySelector('textarea').value = 'Foo'; + vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(vm.handleUpdate).toHaveBeenCalled(); + }); }); }); diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index f38f0776303..7e17457ce70 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -8,7 +8,8 @@ describe Banzai::Filter::RelativeLinkFilter do group: group, project_wiki: project_wiki, ref: ref, - requested_path: requested_path + requested_path: requested_path, + only_path: only_path }) described_class.call(doc, contexts) @@ -37,6 +38,7 @@ describe Banzai::Filter::RelativeLinkFilter do let(:commit) { project.commit(ref) } let(:project_wiki) { nil } let(:requested_path) { '/' } + let(:only_path) { true } shared_examples :preserve_unchanged do it 'does not modify any relative URL in anchor' do @@ -240,26 +242,35 @@ describe Banzai::Filter::RelativeLinkFilter do let(:commit) { nil } let(:ref) { nil } let(:requested_path) { nil } + let(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' } + let(:relative_path) { "/#{project.full_path}#{upload_path}" } context 'to a project upload' do + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'rewrites the link correctly' do + doc = filter(link(upload_path)) + + expect(doc.at_css('a')['href']).to eq(absolute_path) + end + end + it 'rebuilds relative URL for a link' do - doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('a')['href']) - .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + doc = filter(link(upload_path)) + expect(doc.at_css('a')['href']).to eq(relative_path) - doc = filter(nested(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))) - expect(doc.at_css('a')['href']) - .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + doc = filter(nested(link(upload_path))) + expect(doc.at_css('a')['href']).to eq(relative_path) end it 'rebuilds relative URL for an image' do - doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) - expect(doc.at_css('img')['src']) - .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + doc = filter(image(upload_path)) + expect(doc.at_css('img')['src']).to eq(relative_path) - doc = filter(nested(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))) - expect(doc.at_css('img')['src']) - .to eq "/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + doc = filter(nested(image(upload_path))) + expect(doc.at_css('img')['src']).to eq(relative_path) end it 'does not modify absolute URL' do @@ -288,6 +299,17 @@ describe Banzai::Filter::RelativeLinkFilter do let(:project) { nil } let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" } + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'rewrites the link correctly' do + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(absolute_path) + end + end + it 'rewrites the link correctly' do doc = filter(upload_link) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 27e9c477d61..8ff82c4f791 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1910,38 +1910,44 @@ describe MergeRequest do end describe '#rebase_in_progress?' do - # Create merge request and project before we stub file calls - before do - subject - end + shared_examples 'checking whether a rebase is in progress' do + let(:repo_path) { subject.source_project.repository.path } + let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } - it 'returns true when there is a current rebase directory' do - allow(File).to receive(:exist?).and_return(true) - allow(File).to receive(:mtime).and_return(Time.now) + before do + system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) + end - expect(subject.rebase_in_progress?).to be_truthy - end + it 'returns true when there is a current rebase directory' do + expect(subject.rebase_in_progress?).to be_truthy + end - it 'returns false when there is no rebase directory' do - allow(File).to receive(:exist?).and_return(false) + it 'returns false when there is no rebase directory' do + FileUtils.rm_rf(rebase_path) - expect(subject.rebase_in_progress?).to be_falsey - end + expect(subject.rebase_in_progress?).to be_falsey + end + + it 'returns false when the rebase directory has expired' do + time = 20.minutes.ago.to_time + File.utime(time, time, rebase_path) - it 'returns false when the rebase directory has expired' do - allow(File).to receive(:exist?).and_return(true) - allow(File).to receive(:mtime).and_return(20.minutes.ago) + expect(subject.rebase_in_progress?).to be_falsey + end - expect(subject.rebase_in_progress?).to be_falsey + it 'returns false when the source project has been removed' do + allow(subject).to receive(:source_project).and_return(nil) + + expect(subject.rebase_in_progress?).to be_falsey + end end - it 'returns false when the source project has been removed' do - allow(subject).to receive(:source_project).and_return(nil) - allow(File).to receive(:exist?).and_return(true) - allow(File).to receive(:mtime).and_return(Time.now) + context 'when Gitaly rebase_in_progress is enabled' do + it_behaves_like 'checking whether a rebase is in progress' + end - expect(File).not_to have_received(:exist?) - expect(subject.rebase_in_progress?).to be_falsey + context 'when Gitaly rebase_in_progress is enabled', :disable_gitaly do + it_behaves_like 'checking whether a rebase is in progress' end end end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index ddad6862a63..8a3b1034f3c 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -16,6 +16,66 @@ describe Route do it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_uniqueness_of(:path).case_insensitive } + + describe '#ensure_permanent_paths' do + context 'when the route is not yet persisted' do + let(:new_route) { described_class.new(path: 'foo', source: build(:group)) } + + context 'when permanent conflicting redirects exist' do + it 'is invalid' do + redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz') + redirect.save!(validate: false) + + expect(new_route.valid?).to be_falsey + expect(new_route.errors.first[1]).to eq('foo has been taken before. Please use another one') + end + end + + context 'when no permanent conflicting redirects exist' do + it 'is valid' do + expect(new_route.valid?).to be_truthy + end + end + end + + context 'when path has changed' do + before do + route.path = 'foo' + end + + context 'when permanent conflicting redirects exist' do + it 'is invalid' do + redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz') + redirect.save!(validate: false) + + expect(route.valid?).to be_falsey + expect(route.errors.first[1]).to eq('foo has been taken before. Please use another one') + end + end + + context 'when no permanent conflicting redirects exist' do + it 'is valid' do + expect(route.valid?).to be_truthy + end + end + end + + context 'when path has not changed' do + context 'when permanent conflicting redirects exist' do + it 'is valid' do + redirect = build(:redirect_route, :permanent, path: 'git_lab/foo/bar') + redirect.save!(validate: false) + + expect(route.valid?).to be_truthy + end + end + context 'when no permanent conflicting redirects exist' do + it 'is valid' do + expect(route.valid?).to be_truthy + end + end + end + end end describe 'callbacks' do diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index 5f047e61c31..fc1c3d67203 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -36,7 +36,7 @@ describe MergeRequests::RebaseService do end end - context 'when unexpected error occurs' do + context 'when unexpected error occurs', :disable_gitaly do before do allow(repository).to receive(:run_git!).and_raise('Something went wrong') end @@ -53,7 +53,7 @@ describe MergeRequests::RebaseService do end end - context 'with git command failure' do + context 'with git command failure', :disable_gitaly do before do allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong') end @@ -71,31 +71,41 @@ describe MergeRequests::RebaseService do end context 'valid params' do - before do - service.execute(merge_request) - end + shared_examples 'successful rebase' do + before do + service.execute(merge_request) + end - it 'rebases source branch' do - parent_sha = merge_request.source_project.repository.commit(merge_request.source_branch).parents.first.sha - target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha - expect(parent_sha).to eq(target_branch_sha) - end + it 'rebases source branch' do + parent_sha = merge_request.source_project.repository.commit(merge_request.source_branch).parents.first.sha + target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end - it 'records the new SHA on the merge request' do - head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha - expect(merge_request.reload.rebase_commit_sha).to eq(head_sha) + it 'records the new SHA on the merge request' do + head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha + expect(merge_request.reload.rebase_commit_sha).to eq(head_sha) + end + + it 'logs correct author and commiter' do + head_commit = merge_request.source_project.repository.commit(merge_request.source_branch) + + expect(head_commit.author_email).to eq('dmitriy.zaporozhets@gmail.com') + expect(head_commit.author_name).to eq('Dmitriy Zaporozhets') + expect(head_commit.committer_email).to eq(user.email) + expect(head_commit.committer_name).to eq(user.name) + end end - it 'logs correct author and commiter' do - head_commit = merge_request.source_project.repository.commit(merge_request.source_branch) + context 'when Gitaly rebase feature is enabled' do + it_behaves_like 'successful rebase' + end - expect(head_commit.author_email).to eq('dmitriy.zaporozhets@gmail.com') - expect(head_commit.author_name).to eq('Dmitriy Zaporozhets') - expect(head_commit.committer_email).to eq(user.email) - expect(head_commit.committer_name).to eq(user.name) + context 'when Gitaly rebase feature is disabled', :disable_gitaly do + it_behaves_like 'successful rebase' end - context 'git commands' do + context 'git commands', :disable_gitaly do it 'sets GL_REPOSITORY env variable when calling git commands' do expect(repository).to receive(:popen).exactly(3) .with(anything, anything, hash_including('GL_REPOSITORY')) @@ -106,27 +116,37 @@ describe MergeRequests::RebaseService do end context 'fork' do - let(:forked_project) do - fork_project(project, user, repository: true) + shared_examples 'successful fork rebase' do + let(:forked_project) do + fork_project(project, user, repository: true) + end + + let(:merge_request_from_fork) do + forked_project.repository.create_file( + user, + 'new-file-to-target', + '', + message: 'Add new file to target', + branch_name: 'master') + + create(:merge_request, + source_branch: 'master', source_project: forked_project, + target_branch: 'master', target_project: project) + end + + it 'rebases source branch' do + parent_sha = forked_project.repository.commit(merge_request_from_fork.source_branch).parents.first.sha + target_branch_sha = project.repository.commit(merge_request_from_fork.target_branch).sha + expect(parent_sha).to eq(target_branch_sha) + end end - let(:merge_request_from_fork) do - forked_project.repository.create_file( - user, - 'new-file-to-target', - '', - message: 'Add new file to target', - branch_name: 'master') - - create(:merge_request, - source_branch: 'master', source_project: forked_project, - target_branch: 'master', target_project: project) + context 'when Gitaly rebase feature is enabled' do + it_behaves_like 'successful fork rebase' end - it 'rebases source branch' do - parent_sha = forked_project.repository.commit(merge_request_from_fork.source_branch).parents.first.sha - target_branch_sha = project.repository.commit(merge_request_from_fork.target_branch).sha - expect(parent_sha).to eq(target_branch_sha) + context 'when Gitaly rebase feature is disabled', :disable_gitaly do + it_behaves_like 'successful fork rebase' end end end |