From 3bd9ad5574f2ee81888dc13bc29e1d66dafaedba Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 25 Mar 2021 03:09:35 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- Gemfile | 3 - Gemfile.lock | 4 - .../shared/wikis/components/wiki_form.vue | 253 ++++++++++++ .../javascripts/pages/shared/wikis/index.js | 23 ++ .../javascripts/pages/shared/wikis/wikis.js | 75 ---- .../pages/users/activity_calendar.js | 2 +- .../vue_shared/components/markdown/field.vue | 6 + .../admin/application_settings_controller.rb | 2 +- .../import/gitlab_projects_controller.rb | 2 +- app/controllers/import/manifest_controller.rb | 2 +- app/controllers/projects/forks_controller.rb | 2 +- app/controllers/projects/issues_controller.rb | 8 +- .../merge_requests/creations_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 4 +- .../projects/network_controller.rb | 2 +- app/controllers/projects/notes_controller.rb | 2 +- .../projects/pipelines_controller.rb | 4 +- app/controllers/registrations_controller.rb | 2 +- app/graphql/mutations/issues/move.rb | 2 +- .../mutations/merge_requests/accept.rb | 2 +- .../project_services/hipchat_service.rb | 166 +------- app/services/post_receive_service.rb | 2 +- app/views/shared/wikis/_form.html.haml | 79 +--- ...a-backup-repository-empty-repo-skipped.yml | 5 + changelogs/unreleased/remove_hipchat_gem.yml | 5 + .../update-contribution-tooltip.yml | 5 + config/initializers/hipchat_client_patch.rb | 15 - doc/administration/job_artifacts.md | 22 +- doc/administration/pages/index.md | 2 +- doc/ci/pipelines/job_artifacts.md | 374 ++++++++---------- doc/ci/yaml/README.md | 4 +- .../coverage_fuzzing/index.md | 2 +- doc/user/application_security/sast/index.md | 2 +- doc/user/project/integrations/hipchat.md | 7 +- .../merge_requests/accessibility_testing.md | 2 +- .../project/merge_requests/code_quality.md | 2 +- doc/user/project/pages/introduction.md | 2 +- lib/api/ci/pipelines.rb | 2 +- lib/api/groups.rb | 2 +- lib/api/issues.rb | 6 +- lib/api/merge_requests.rb | 6 +- lib/api/project_import.rb | 2 +- lib/api/projects.rb | 2 +- lib/api/triggers.rb | 2 +- lib/backup/repositories.rb | 2 +- locale/gitlab.pot | 55 +-- qa/qa/page/component/wiki_page_form.rb | 13 +- .../project_based_content_creation_spec.rb | 4 +- ...project_based_content_manipulation_spec.rb | 2 +- ...project_based_directory_management_spec.rb | 2 +- scripts/perf/query_limiting_report.rb | 167 ++++++++ spec/features/calendar_spec.rb | 2 +- .../wikis/{ => components}/wiki_alert_spec.js | 0 .../shared/wikis/components/wiki_form_spec.js | 222 +++++++++++ .../snippet_description_edit_spec.js.snap | 1 + spec/frontend/wikis_spec.js | 153 ------- .../project_services/hipchat_service_spec.rb | 321 +-------------- .../user_creates_wiki_page_shared_examples.rb | 2 +- ...r_previews_wiki_changes_shared_examples.rb | 6 +- .../user_updates_wiki_page_shared_examples.rb | 6 +- .../user_views_wiki_empty_shared_examples.rb | 2 +- 61 files changed, 976 insertions(+), 1104 deletions(-) create mode 100644 app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue create mode 100644 changelogs/unreleased/hchouraria-backup-repository-empty-repo-skipped.yml create mode 100644 changelogs/unreleased/remove_hipchat_gem.yml create mode 100644 changelogs/unreleased/update-contribution-tooltip.yml delete mode 100644 config/initializers/hipchat_client_patch.rb create mode 100755 scripts/perf/query_limiting_report.rb rename spec/frontend/pages/shared/wikis/{ => components}/wiki_alert_spec.js (100%) create mode 100644 spec/frontend/pages/shared/wikis/components/wiki_form_spec.js diff --git a/Gemfile b/Gemfile index e0cc20cc91b..a64bc0c698b 100644 --- a/Gemfile +++ b/Gemfile @@ -238,9 +238,6 @@ gem 'redis-rails', '~> 5.0.2' # Discord integration gem 'discordrb-webhooks', '~> 3.4', require: false -# HipChat integration -gem 'hipchat', '~> 1.5.0' - # Jira integration gem 'jira-ruby', '~> 2.1.4' gem 'atlassian-jwt', '~> 0.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 891ed84c940..ca4b0151798 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -593,9 +593,6 @@ GEM railties (>= 5.0) heapy (0.2.0) thor - hipchat (1.5.2) - httparty - mimemagic html-pipeline (2.13.2) activesupport (>= 2) nokogiri (>= 1.4) @@ -1456,7 +1453,6 @@ DEPENDENCIES hashie hashie-forbidden_attributes health_check (~> 3.0) - hipchat (~> 1.5.0) html-pipeline (~> 2.13.2) html2text httparty (~> 0.16.4) diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue new file mode 100644 index 00000000000..d84f967bbb1 --- /dev/null +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -0,0 +1,253 @@ + + + diff --git a/app/assets/javascripts/pages/shared/wikis/index.js b/app/assets/javascripts/pages/shared/wikis/index.js index c382a372260..c04cd0b3fa4 100644 --- a/app/assets/javascripts/pages/shared/wikis/index.js +++ b/app/assets/javascripts/pages/shared/wikis/index.js @@ -1,12 +1,14 @@ import $ from 'jquery'; import Vue from 'vue'; import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import csrf from '~/lib/utils/csrf'; import Translate from '~/vue_shared/translate'; import GLForm from '../../../gl_form'; import ZenMode from '../../../zen_mode'; import deleteWikiModal from './components/delete_wiki_modal.vue'; import wikiAlert from './components/wiki_alert.vue'; +import wikiForm from './components/wiki_form.vue'; import Wikis from './wikis'; const createModalVueApp = () => { @@ -61,7 +63,28 @@ const createAlertVueApp = () => { } }; +const createWikiFormApp = () => { + const el = document.getElementById('js-wiki-form'); + + if (el) { + const { pageInfo, formatOptions } = el.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el, + provide: { + formatOptions: JSON.parse(formatOptions), + pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)), + }, + render(createElement) { + return createElement(wikiForm); + }, + }); + } +}; + export default () => { createModalVueApp(); createAlertVueApp(); + createWikiFormApp(); }; diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js index 4b4d2f7d238..7d0b0c90c8d 100644 --- a/app/assets/javascripts/pages/shared/wikis/wikis.js +++ b/app/assets/javascripts/pages/shared/wikis/wikis.js @@ -1,15 +1,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import { s__, sprintf } from '~/locale'; import Tracking from '~/tracking'; import showToast from '~/vue_shared/plugins/global_toast'; -const MARKDOWN_LINK_TEXT = { - markdown: '[Link Title](page-slug)', - rdoc: '{Link title}[link:page-slug]', - asciidoc: 'link:page-slug[Link title]', - org: '[[page-slug]]', -}; - const TRACKING_EVENT_NAME = 'view_wiki_page'; const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/wiki_page_context/jsonschema/1-0-1'; @@ -23,78 +15,11 @@ export default class Wikis { sidebarToggles[i].addEventListener('click', (e) => this.handleToggleSidebar(e)); } - this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page')); - this.editTitleInput = document.querySelector('form.wiki-form #wiki_title'); - this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message'); - this.submitButton = document.querySelector('.js-wiki-btn-submit'); - this.commitMessageI18n = this.isNewWikiPage - ? s__('WikiPageCreate|Create %{pageTitle}') - : s__('WikiPageEdit|Update %{pageTitle}'); - - if (this.editTitleInput) { - // Initialize the commit message on load - if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value); - - // Set the commit message as the page title is changed - this.editTitleInput.addEventListener('keyup', (e) => this.handleWikiTitleChange(e)); - } - window.addEventListener('resize', () => this.renderSidebar()); this.renderSidebar(); - const changeFormatSelect = document.querySelector('#wiki_format'); - const linkExample = document.querySelector('.js-markup-link-example'); - - if (changeFormatSelect) { - changeFormatSelect.addEventListener('change', (e) => { - linkExample.innerHTML = MARKDOWN_LINK_TEXT[e.target.value]; - }); - } - - this.wikiTextarea = document.querySelector('form.wiki-form #wiki_content'); - const wikiForm = document.querySelector('form.wiki-form'); - - if (this.wikiTextarea) { - this.wikiTextarea.addEventListener('input', () => this.handleWikiContentChange()); - - wikiForm.addEventListener('submit', () => { - window.onbeforeunload = null; - }); - } - Wikis.trackPageView(); Wikis.showToasts(); - - this.updateSubmitButton(); - } - - handleWikiContentChange() { - this.updateSubmitButton(); - - window.onbeforeunload = () => ''; - } - - handleWikiTitleChange(e) { - this.updateSubmitButton(); - this.setWikiCommitMessage(e.target.value); - } - - updateSubmitButton() { - if (!this.wikiTextarea) return; - - const isEnabled = Boolean(this.wikiTextarea.value.trim() && this.editTitleInput.value.trim()); - if (isEnabled) this.submitButton.removeAttribute('disabled'); - else this.submitButton.setAttribute('disabled', 'true'); - } - - setWikiCommitMessage(rawTitle) { - let title = rawTitle; - - // Replace hyphens with spaces - if (title) title = title.replace(/-+/g, ' '); - - const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title }, false); - this.commitMessageInput.value = newCommitMessage; } handleToggleSidebar(e) { diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 3ff455fad32..d236dc4610a 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -39,7 +39,7 @@ function formatTooltipText({ date, count }) { if (count > 0) { contribText = n__('%d contribution', '%d contributions', count); } - return `${contribText}
${dateDayName} ${dateText}`; + return `${contribText}
${dateDayName} ${dateText}`; } const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]); diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 25d01dc550f..220570cd8a0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -62,6 +62,11 @@ export default { required: false, default: true, }, + uploadsPath: { + type: String, + required: false, + default: '', + }, enableAutocomplete: { type: Boolean, required: false, @@ -229,6 +234,7 @@ export default { ref="gl-form" :class="{ 'gl-mt-3 gl-mb-3': addSpacingClasses }" class="js-vue-markdown-field md-area position-relative gfm-form" + :data-uploads-path="uploadsPath" > #{ref}"\ - " to #{project_link}\n" - elsif Gitlab::Git.blank_ref?(after) - message << "removed #{ref_type} #{ref} from #{project_name} \n" - else - message << "pushed to #{ref_type} #{ref} " - message << "of #{project.full_name.gsub!(/\s/, '')} " - message << "(Compare changes)" - - push[:commits].take(MAX_COMMITS).each do |commit| - message << "
- #{render_line(commit[:message])} (#{commit[:id][0..5]})" - end - - if push[:commits].count > MAX_COMMITS - message << "
... #{push[:commits].count - MAX_COMMITS} more commits" - end - end - - message.join - end - def markdown(text, options = {}) return "" unless text @@ -155,109 +94,10 @@ class HipchatService < Service sanitized_html.truncate(200, separator: ' ', omission: '...') end - def create_issue_message(data) - user_name = data[:user][:name] - - obj_attr = data[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - title = render_line(obj_attr[:title]) - state = Issue.available_states.key(obj_attr[:state_id]) - issue_iid = obj_attr[:iid] - issue_url = obj_attr[:url] - description = obj_attr[:description] - - issue_link = "issue ##{issue_iid}" - - message = ["#{user_name} #{state} #{issue_link} in #{project_link}: #{title}"] - message << "
#{markdown(description)}
" - - message.join - end - - def create_merge_request_message(data) - user_name = data[:user][:name] - - obj_attr = data[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - merge_request_id = obj_attr[:iid] - state = obj_attr[:state] - description = obj_attr[:description] - title = render_line(obj_attr[:title]) - - merge_request_url = "#{project_url}/-/merge_requests/#{merge_request_id}" - merge_request_link = "merge request !#{merge_request_id}" - message = ["#{user_name} #{state} #{merge_request_link} in " \ - "#{project_link}: #{title}"] - - message << "
#{markdown(description)}
" - message.join - end - def format_title(title) "#{render_line(title)}" end - def create_note_message(data) - data = HashWithIndifferentAccess.new(data) - user_name = data[:user][:name] - - obj_attr = HashWithIndifferentAccess.new(data[:object_attributes]) - note = obj_attr[:note] - note_url = obj_attr[:url] - noteable_type = obj_attr[:noteable_type] - commit_id = nil - - case noteable_type - when "Commit" - commit_attr = HashWithIndifferentAccess.new(data[:commit]) - commit_id = commit_attr[:id] - subject_desc = commit_id - subject_desc = Commit.truncate_sha(subject_desc) - subject_type = "commit" - title = format_title(commit_attr[:message]) - when "Issue" - subj_attr = HashWithIndifferentAccess.new(data[:issue]) - subject_id = subj_attr[:iid] - subject_desc = "##{subject_id}" - subject_type = "issue" - title = format_title(subj_attr[:title]) - when "MergeRequest" - subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) - subject_id = subj_attr[:iid] - subject_desc = "!#{subject_id}" - subject_type = "merge request" - title = format_title(subj_attr[:title]) - when "Snippet" - subj_attr = HashWithIndifferentAccess.new(data[:snippet]) - subject_id = subj_attr[:id] - subject_desc = "##{subject_id}" - subject_type = "snippet" - title = format_title(subj_attr[:title]) - end - - subject_html = "#{subject_type} #{subject_desc}" - message = ["#{user_name} commented on #{subject_html} in #{project_link}: "] - message << title - - message << "
#{markdown(note, ref: commit_id)}
" - message.join - end - - def create_pipeline_message(data) - pipeline_attributes = data[:object_attributes] - pipeline_id = pipeline_attributes[:id] - ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' - ref = pipeline_attributes[:ref] - user_name = (data[:user] && data[:user][:name]) || 'API' - status = pipeline_attributes[:status] - duration = pipeline_attributes[:duration] - - branch_link = "#{ref}" - pipeline_url = "##{pipeline_id}" - - "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" - end - def message_color(data) pipeline_status_color(data) || color || 'yellow' end @@ -309,5 +149,3 @@ class HipchatService < Service end end end - -HipchatService.prepend_if_ee('EE::HipchatService') diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb index 24b19e75639..3dc8fd8929a 100644 --- a/app/services/post_receive_service.rb +++ b/app/services/post_receive_service.rb @@ -48,7 +48,7 @@ class PostReceiveService end def process_mr_push_options(push_options, changes) - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/61359') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/28494') return unless repository unless repository.repo_type.project? diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml index d91e3c73c49..e121725b9af 100644 --- a/app/views/shared/wikis/_form.html.haml +++ b/app/views/shared/wikis/_form.html.haml @@ -1,79 +1,6 @@ -- form_classes = %w[wiki-form common-note-form gl-mt-3 js-quick-submit] +- page_info = { last_commit_sha: @page.last_commit_sha, persisted: @page.persisted?, title: @page.title, content: @page.content || '', format: @page.format.to_s, uploads_path: uploads_path, path: wiki_page_path(@wiki, @page), wiki_path: wiki_path(@wiki), help_path: help_page_path('user/project/wiki/index'), markdown_help_path: help_page_path('user/markdown'), markdown_preview_path: wiki_page_path(@wiki, @page, action: :preview_markdown), create_path: wiki_path(@wiki, action: :create) } -- if @page.persisted? - - form_action = wiki_page_path(@wiki, @page) - - form_method = :put -- else - - form_action = wiki_path(@wiki, action: :create) - - form_method = :post - - form_classes << 'js-new-wiki-page' - -= form_for @page, url: form_action, method: form_method, - html: { class: form_classes }, - data: { uploads_path: uploads_path } do |f| +.gl-mt-3 = form_errors(@page, truncate: :title) - - if @page.persisted? - = f.hidden_field :last_commit_sha, value: @page.last_commit_sha - - .form-group.row - .col-sm-2.col-form-label= f.label :title, class: 'control-label-full-width' - .col-sm-10 - = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: s_('Wiki|Page title') - %span.gl-display-inline-block.gl-max-w-full.gl-mt-2.gl-text-gray-600 - = sprite_icon('bulb', size: 12, css_class: 'gl-mr-n1') - - if @page.persisted? - = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") - = link_to sprite_icon('question-o'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), - target: '_blank', rel: 'noopener noreferrer' - - else - = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") - = succeed '.' do - = link_to _('More information'), help_page_path('user/project/wiki/index', anchor: 'creating-a-new-wiki-page'), - target: '_blank', rel: 'noopener noreferrer' - .form-group.row - .col-sm-2.col-form-label= f.label :format, class: 'control-label-full-width' - .col-sm-10 - .select-wrapper - = f.select :format, options_for_select(Wiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control select-control' - = sprite_icon('chevron-down', css_class: 'gl-absolute gl-top-3 gl-right-3 gl-text-gray-200') - - .form-group.row - .col-sm-2.col-form-label= f.label :content, class: 'control-label-full-width' - .col-sm-10 - = render layout: 'shared/md_preview', locals: { url: wiki_page_path(@wiki, @page, action: :preview_markdown) } do - = render 'shared/zen', f: f, attr: :content, classes: 'note-textarea qa-wiki-content-textarea', placeholder: s_("WikiPage|Write your content or drag files here…"), autofocus: @page.persisted? - = render 'shared/notes/hints' - - .clearfix - .error-alert - - .form-text.gl-text-gray-600 - = succeed '.' do - - case @page.format.to_s - - when 'rdoc' - - link_example = '{Link title}[link:page-slug]' - - when 'asciidoc' - - link_example = 'link:page-slug[Link title]' - - when 'org' - - link_example = '[[page-slug]]' - - else - - link_example = '[Link Title](page-slug)' - = html_escape(s_('WikiMarkdownTip|To link to a (new) page, simply type %{link_example}')) % { link_example: tag.code(link_example, class: 'js-markup-link-example') } - = succeed '.' do - - markdown_link = link_to s_("WikiMarkdownDocs|documentation"), help_page_path('user/markdown', anchor: 'wiki-specific-markdown') - = (s_("WikiMarkdownDocs|More examples are in the %{docs_link}") % { docs_link: markdown_link }).html_safe - - .form-group.row - .col-sm-2.col-form-label= f.label :commit_message, class: 'control-label-full-width' - .col-sm-10= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: nil - - .form-actions - - if @page && @page.persisted? - = f.submit _("Save changes"), class: 'btn gl-button btn-confirm qa-save-changes-button js-wiki-btn-submit', disabled: 'true' - .float-right - = link_to _("Cancel"), wiki_page_path(@wiki, @page), class: 'btn gl-button btn-cancel btn-default' - - else - = f.submit s_("Wiki|Create page"), class: 'btn-confirm gl-button btn qa-create-page-button rspec-create-page-button js-wiki-btn-submit', disabled: 'true' - .float-right - = link_to _("Cancel"), wiki_path(@wiki), class: 'btn gl-button btn-cancel btn-default' +#js-wiki-form{ data: { page_info: page_info.to_json, format_options: Wiki::MARKUPS.to_json } } diff --git a/changelogs/unreleased/hchouraria-backup-repository-empty-repo-skipped.yml b/changelogs/unreleased/hchouraria-backup-repository-empty-repo-skipped.yml new file mode 100644 index 00000000000..272457a0059 --- /dev/null +++ b/changelogs/unreleased/hchouraria-backup-repository-empty-repo-skipped.yml @@ -0,0 +1,5 @@ +--- +title: Add message for repository backup skip +merge_request: 54285 +author: +type: other diff --git a/changelogs/unreleased/remove_hipchat_gem.yml b/changelogs/unreleased/remove_hipchat_gem.yml new file mode 100644 index 00000000000..21a5db3bb5a --- /dev/null +++ b/changelogs/unreleased/remove_hipchat_gem.yml @@ -0,0 +1,5 @@ +--- +title: Make HipChat project service do nothing +merge_request: 57434 +author: +type: removed diff --git a/changelogs/unreleased/update-contribution-tooltip.yml b/changelogs/unreleased/update-contribution-tooltip.yml new file mode 100644 index 00000000000..d1962a5cac0 --- /dev/null +++ b/changelogs/unreleased/update-contribution-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Add multi-line styling within contribution tooltip +merge_request: 54765 +author: Yogi (@yo) +type: changed diff --git a/config/initializers/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb deleted file mode 100644 index 51bd48af320..00000000000 --- a/config/initializers/hipchat_client_patch.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true -# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb. -module HipChat - class Client - connection_adapter ::Gitlab::HTTPConnectionAdapter - end - - class Room - connection_adapter ::Gitlab::HTTPConnectionAdapter - end - - class User - connection_adapter ::Gitlab::HTTPConnectionAdapter - end -end diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 661fa67220d..7fd457cd07d 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -89,7 +89,7 @@ _The artifacts are stored by default in > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1762) in > [GitLab Premium](https://about.gitlab.com/pricing/) 9.4. -> - Since version 9.5, artifacts are [browsable](../ci/pipelines/job_artifacts.md#browsing-artifacts), +> - Since version 9.5, artifacts are [browsable](../ci/pipelines/job_artifacts.md#download-job-artifacts), > when object storage is enabled. 9.4 lacks this feature. > - Since version 10.6, available in [GitLab Free](https://about.gitlab.com/pricing/). > - Since version 11.0, we support `direct_upload` to S3. @@ -509,7 +509,7 @@ If you need to manually remove job artifacts associated with multiple jobs while NOTE: This step also erases artifacts that users have chosen to - ["keep"](../ci/pipelines/job_artifacts.md#browsing-artifacts). + ["keep"](../ci/pipelines/job_artifacts.md#download-job-artifacts). ```ruby builds_to_clear = builds_with_artifacts.where("finished_at < ?", 1.week.ago) @@ -583,3 +583,21 @@ If you need to manually remove **all** job artifacts associated with multiple jo - `7.days.ago` - `3.months.ago` - `1.year.ago` + +### Error `Downloading artifacts from coordinator... not found` + +When a job tries to download artifacts from an earlier job, you might receive an error similar to: + +```plaintext +Downloading artifacts from coordinator... not found id=12345678 responseStatus=404 Not Found +``` + +This might be caused by a `gitlab.rb` file with the following configuration: + +```ruby +gitlab_rails['artifacts_object_store_background_upload'] = false +gitlab_rails['artifacts_object_store_direct_upload'] = true +``` + +To prevent this, comment out or remove those lines, or switch to their [default values](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template), +then run `sudo gitlab-ctl reconfigure`. diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 6fba192e794..8181c13aca0 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -487,7 +487,7 @@ internet connectivity is gated by a proxy. To use a proxy for GitLab Pages: ### Using a custom Certificate Authority (CA) When using certificates issued by a custom CA, [Access Control](../../user/project/pages/pages_access_control.md#gitlab-pages-access-control) and -the [online view of HTML job artifacts](../../ci/pipelines/job_artifacts.md#browsing-artifacts) +the [online view of HTML job artifacts](../../ci/pipelines/job_artifacts.md#download-job-artifacts) fails to work if the custom CA is not recognized. This usually results in this error: diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index cf8e4e71534..72266cce3e6 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -8,25 +8,19 @@ type: reference, howto # Job artifacts -> - Introduced in GitLab 8.2 and GitLab Runner 0.7.0. -> - Starting with GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format changed to `ZIP`, and it's now possible to browse its contents, with the added ability of downloading the files separately. -> - In GitLab 8.17, builds were renamed to jobs. -> - The artifacts browser is available only for new artifacts that are sent to GitLab using GitLab Runner version 1.0 and up. You cannot browse old artifacts already uploaded to GitLab. +> Introduced in [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16675), artifacts in internal and private projects can be previewed when [GitLab Pages access control](../../administration/pages/index.md#access-control) is enabled. -Job artifacts are a list of files and directories created by a job -once it finishes. This feature is [enabled by default](../../administration/job_artifacts.md) in all -GitLab installations. +Jobs can output an archive of files and directories. This output is known as a job artifact. -Job artifacts created by GitLab Runner are uploaded to GitLab and are downloadable as a single archive using the GitLab UI or the [GitLab API](../../api/job_artifacts.md#get-job-artifacts). +You can download job artifacts by using the GitLab UI or the [API](../../api/job_artifacts.md#get-job-artifacts). For an overview, watch the video [GitLab CI Pipeline, Artifacts, and Environments](https://www.youtube.com/watch?v=PCKDICEe10s). Watch also [GitLab CI pipeline tutorial for beginners](https://www.youtube.com/watch?v=Jav4vbUrqII). -## Defining artifacts in `.gitlab-ci.yml` +## Define artifacts in the `.gitlab-ci.yml` file -A simple example of using the artifacts definition in `.gitlab-ci.yml` would be -the following: +This example shows how to configure your `.gitlab-ci.yml` file to create job artifacts: ```yaml pdf: @@ -38,19 +32,169 @@ pdf: ``` A job named `pdf` calls the `xelatex` command to build a PDF file from the -latex source file `mycv.tex`. We then define the `artifacts` paths which in -turn are defined with the `paths` keyword. All paths to files and directories -are relative to the repository that was cloned during the build. +LaTeX source file, `mycv.tex`. -By default, the artifacts upload when the job succeeds. You can also set artifacts to upload -when the job fails, or always, by using [`artifacts:when`](../yaml/README.md#artifactswhen) -keyword. GitLab keeps these uploaded artifacts for 1 week, as defined -by the `expire_in` definition. You can keep the artifacts from expiring -via the [web interface](#browsing-artifacts). If the expiry time is not defined, it defaults -to the [instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration). +The `paths` keyword determines which files to add to the job artifacts. +All paths to files and directories are relative to the repository where the job was created. -For more examples on artifacts, follow the [artifacts reference in -`.gitlab-ci.yml`](../yaml/README.md#artifacts). +The `expire_in` keyword determines how long GitLab keeps the job artifacts. +You can also [use the UI to keep job artifacts from expiring](#download-job-artifacts). +If `expire_in` is not defined, the +[instance-wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration) +is used. + +For more examples, view the [keyword reference for the `.gitlab-ci.yml` file](../yaml/README.md#artifacts). + +## Download job artifacts + +You can download job artifacts or view the job archive: + +- On the **Pipelines** page, to the right of the pipeline: + + ![Job artifacts in Pipelines page](img/job_artifacts_pipelines_page.png) + +- On the **Jobs** page, to the right of the job: + + ![Job artifacts in Builds page](img/job_artifacts_builds_page.png) + +- On a job's detail page. The **Keep** button indicates an `expire_in` value was set: + + ![Job artifacts browser button](img/job_artifacts_browser_button.png) + +- On a merge request, by the pipeline details: + + ![Job artifacts in Merge Request](img/job_artifacts_merge_request.png) + +- When browsing an archive: + + ![Job artifacts browser](img/job_artifacts_browser.png) + + If [GitLab Pages](../../administration/pages/index.md) is enabled in the project, you can preview + HTML files in the artifacts directly in your browser. If the project is internal or private, you must + enable [GitLab Pages access control](../../administration/pages/index.md#access-control) to preview + HTML files. + +## View failed job artifacts + +If the latest job has failed to upload the artifacts, you can see that +information in the UI. + +![Latest artifacts button](img/job_latest_artifacts_browser.png) + +## Erase job artifacts + +WARNING: +This is a destructive action that leads to data loss. Use with caution. + +You can erase a single job, which also removes the job's +artifacts and log. You must be: + +- The owner of the job. +- A [Maintainer](../../user/permissions.md#gitlab-cicd-permissions) of the project. + +To erase a job: + +1. Go to a job's detail page. +1. At the top right of the job's log, select the trash icon. +1. Confirm the deletion. + +## Use GitLab CI/CD to retrieve job artifacts for private projects + +To retrieve a job artifact from a different project, you might need to use a +private token to [authenticate and download](../../api/job_artifacts.md#get-job-artifacts) +the artifact. + +## The latest job artifacts + +Job artifacts created in the most recent successful pipeline for a specific ref +are considered the latest artifacts. If you run two types of pipelines for the same ref, +timing determines which artifacts are the latest. + +For example, if a merge request creates a branch pipeline at the same time as +a scheduled pipeline, the pipeline that finished most recently creates the latest job artifact. + +In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201784) and later, artifacts +for [parent and child pipelines](../parent_child_pipelines.md) are searched in hierarchical +order from parent to child. For example, if both parent and child pipelines have a +job with the same name, the job artifact from the parent pipeline is returned. + +## Access the latest job artifacts by URL + +You can download the latest job artifacts by using a URL. + +To download the whole artifacts archive: + +```plaintext +https://example.com///-/jobs/artifacts//download?job= +``` + +To download a single file from the artifacts: + +```plaintext +https://example.com///-/jobs/artifacts//raw/?job= +``` + +For example, to download the latest artifacts of the job named `coverage` of +the `master` branch of the `gitlab` project that belongs to the `gitlab-org` +namespace: + +```plaintext +https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/download?job=coverage +``` + +To download the file `coverage/index.html` from the same artifacts: + +```plaintext +https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/raw/coverage/index.html?job=coverage +``` + +To browse the latest job artifacts: + +```plaintext +https://example.com///-/jobs/artifacts//browse?job= +``` + +For example: + +```plaintext +https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/browse?job=coverage +``` + +There is also a URL for specific files, including HTML files that +are shown in [GitLab Pages](../../administration/pages/index.md): + +```plaintext +https://example.com///-/jobs/artifacts//file/?job= +``` + +For example, when a job `coverage` creates the artifact `htmlcov/index.html`: + +```plaintext +https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/file/htmlcov/index.html?job=coverage +``` + +### Keep artifacts from most recent successful jobs + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16267) in GitLab 13.0. +> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/229936) in GitLab 13.4. +> - [Made optional with a CI/CD setting](https://gitlab.com/gitlab-org/gitlab/-/issues/241026) in GitLab 13.8. + +By default, the latest job artifacts from the most recent successful jobs are never deleted. +If a job is configured with [`expire_in`](../yaml/README.md#artifactsexpire_in), +its artifacts only expire if a more recent artifact exists. + +Keeping the latest artifacts can use a large amount of storage space in projects +with a lot of jobs or large artifacts. If the latest artifacts are not needed in +a project, you can disable this behavior to save space: + +1. Go to the project's **Settings > CI/CD > Artifacts**. +1. Clear the **Keep artifacts from most recent successful jobs** checkbox. + +You can disable this behavior for all projects on a self-managed instance in the +[instance's CI/CD settings](../../user/admin_area/settings/continuous_integration.md#keep-the-latest-artifacts-for-all-jobs-in-the-latest-successful-pipelines). + +When you disable the feature, the latest artifacts do not immediately expire. +A new pipeline must run before the latest artifacts can expire and be deleted. ### `artifacts:reports` @@ -306,191 +450,7 @@ The collected Requirements report uploads to GitLab as an artifact and existing [requirements](../../user/project/requirements/index.md) are marked as Satisfied. -## Browsing artifacts - -> - From GitLab 9.2, PDFs, images, videos, and other formats can be previewed directly in the job artifacts browser without the need to download them. -> - Introduced in [GitLab 10.1](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14399), HTML files in a public project can be previewed directly in a new tab without the need to download them when [GitLab Pages](../../administration/pages/index.md) is enabled. The same applies for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`). -> - Introduced in [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16675), artifacts in internal and private projects can be previewed when [GitLab Pages access control](../../administration/pages/index.md#access-control) is enabled. - -After a job finishes, if you visit the job's specific page, there are three -buttons. You can download the artifacts archive or browse its contents, whereas -the **Keep** button appears only if you've set an [expiry date](../yaml/README.md#artifactsexpire_in) to the -artifacts in case you changed your mind and want to keep them. - -![Job artifacts browser button](img/job_artifacts_browser_button.png) - -The archive browser shows the name and the actual file size of each file in the -archive. If your artifacts contained directories, then you're also able to -browse inside them. - -Below you can see what browsing looks like. In this case we have browsed inside -the archive and at this point there is one directory, a couple files, and -one HTML file that you can view directly online when -[GitLab Pages](../../administration/pages/index.md) is enabled (opens in a new tab). -Select artifacts in internal and private projects can only be previewed when -[GitLab Pages access control](../../administration/pages/index.md#access-control) is enabled. - -![Job artifacts browser](img/job_artifacts_browser.png) - -## Downloading artifacts - -If you need to download an artifact or the whole archive, there are buttons in various places -in the GitLab UI to do this: - -1. While on the pipelines page, you can see the download icon for each job's - artifacts and archive in the right corner: - - ![Job artifacts in Pipelines page](img/job_artifacts_pipelines_page.png) - -1. While on the **Jobs** page, you can see the download icon for each job's - artifacts and archive in the right corner: - - ![Job artifacts in Builds page](img/job_artifacts_builds_page.png) - -1. While inside a specific job, you're presented with a download button - along with the one that browses the archive: - - ![Job artifacts browser button](img/job_artifacts_browser_button.png) - -1. While on the details page of a merge request, you can see the download - icon for each job's artifacts on the right side of the merge request widget: - - ![Job artifacts in Merge Request](img/job_artifacts_merge_request.png) - -1. And finally, when browsing an archive you can see the download button at - the top right corner: - - ![Job artifacts browser](img/job_artifacts_browser.png) - -## Downloading the latest artifacts - -It's 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 created by jobs in the **most recent** successful pipeline -for the specific ref. If you run two types of pipelines for the same ref, timing determines the latest -artifact. For example, if a merge request creates a branch pipeline at the same time as a scheduled pipeline, the pipeline that completed most recently creates the latest artifact. - -In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201784) and later, artifacts -for [parent and child pipelines](../parent_child_pipelines.md) are searched in hierarchical -order from parent to child. For example, if both parent and child pipelines have a -job with the same name, the artifact from the parent pipeline is returned. - -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: - -```plaintext -https://example.com///-/jobs/artifacts//download?job= -``` - -To download a single file from the artifacts use the following URL: - -```plaintext -https://example.com///-/jobs/artifacts//raw/?job= -``` - -For example, to download the latest artifacts of the job named `coverage` of -the `master` branch of the `gitlab` project that belongs to the `gitlab-org` -namespace, the URL would be: - -```plaintext -https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/download?job=coverage -``` - -To download the file `coverage/index.html` from the same -artifacts use the following URL: - -```plaintext -https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/raw/coverage/index.html?job=coverage -``` - -There is also a URL to browse the latest job artifacts: - -```plaintext -https://example.com///-/jobs/artifacts//browse?job= -``` - -For example: - -```plaintext -https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/browse?job=coverage -``` - -There is also a URL to specific files, including HTML files that -are shown in [GitLab Pages](../../administration/pages/index.md): - -```plaintext -https://example.com///-/jobs/artifacts//file/?job= -``` - -For example, when a job `coverage` creates the artifact `htmlcov/index.html`, -you can access it at: - -```plaintext -https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/file/htmlcov/index.html?job=coverage -``` - -The latest builds are also exposed in the UI in various places. Specifically, -look for the download button in: - -- The main project's page -- The branches page -- The tags page - -If the latest job has failed to upload the artifacts, you can see that -information in the UI. - -![Latest artifacts button](img/job_latest_artifacts_browser.png) - -## Erasing artifacts - -WARNING: -This is a destructive action that leads to data loss. Use with caution. - -You can erase a single job via the UI, which also removes the job's -artifacts and trace, if you are: - -- The owner of the job. -- A [Maintainer](../../user/permissions.md#gitlab-cicd-permissions) of the project. - -To erase a job: - -1. Navigate to a job's page. -1. Click the trash icon at the top right of the job's trace. -1. Confirm the deletion. - -## Retrieve artifacts of private projects when using GitLab CI - -To retrieve a job artifact from a different project, you might need to use a -private token to [authenticate and download](../../api/job_artifacts.md#get-job-artifacts) -the artifact. - -## Keep artifacts from most recent successful jobs - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16267) in GitLab 13.0. -> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/229936) in GitLab 13.4. -> - [Made optional with a CI/CD setting](https://gitlab.com/gitlab-org/gitlab/-/issues/241026) in GitLab 13.8. - -By default, the latest artifacts from the most recent successful jobs are never deleted. -If a job is configured with [`expire_in`](../yaml/README.md#artifactsexpire_in), -its artifacts only expire if a more recent artifact exists. - -Keeping the latest artifacts can use a large amount of storage space in projects -with a lot of jobs or large artifacts. If the latest artifacts are not needed in -a project, you can disable this behavior to save space: - -1. Navigate to **Settings > CI/CD > Artifacts**. -1. Uncheck **Keep artifacts from most recent successful jobs**. - -You can disable this behavior for all projects on a self-managed instance in the -[instance's CI/CD settings](../../user/admin_area/settings/continuous_integration.md#keep-the-latest-artifacts-for-all-jobs-in-the-latest-successful-pipelines). **(CORE ONLY)** - -When you disable the feature, the latest artifacts do not immediately expire. -A new pipeline must run before the latest artifacts can expire and be deleted. - -## Troubleshooting +## Troubleshooting job artifacts ### Error message `No files to upload` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5e74fc2027e..940f0c41c8c 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -3239,7 +3239,7 @@ Note the following: - Artifacts do not display in the merge request UI when using variables to define the `artifacts:paths`. - A maximum of 10 job artifacts per merge request can be exposed. - Glob patterns are unsupported. -- If a directory is specified, the link is to the job [artifacts browser](../pipelines/job_artifacts.md#browsing-artifacts) if there is more than +- If a directory is specified, the link is to the job [artifacts browser](../pipelines/job_artifacts.md#download-job-artifacts) if there is more than one file in the directory. - For exposed single file artifacts with `.html`, `.htm`, `.txt`, `.json`, `.xml`, and `.log` extensions, if [GitLab Pages](../../administration/pages/index.md) is: @@ -3508,7 +3508,7 @@ deploy: If the artifacts of the job that is set as a dependency are [expired](#artifactsexpire_in) or -[erased](../pipelines/job_artifacts.md#erasing-artifacts), then +[erased](../pipelines/job_artifacts.md#erase-job-artifacts), then the dependent job fails. You can ask your administrator to diff --git a/doc/user/application_security/coverage_fuzzing/index.md b/doc/user/application_security/coverage_fuzzing/index.md index 94a7d5268b7..b930ee43727 100644 --- a/doc/user/application_security/coverage_fuzzing/index.md +++ b/doc/user/application_security/coverage_fuzzing/index.md @@ -141,7 +141,7 @@ The `gitlab-cov-fuzz` tool emits a JSON report file. For more information, see t [schema for this report](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/coverage-fuzzing-report-format.json). You can download the JSON report file from the CI pipelines page. For more information, see -[Downloading artifacts](../../../ci/pipelines/job_artifacts.md#downloading-artifacts). +[Downloading artifacts](../../../ci/pipelines/job_artifacts.md#download-job-artifacts). Here's an example coverage fuzzing report: diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index ee7f63601d2..3eebd35620a 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -536,7 +536,7 @@ The SAST tool emits a JSON report file. For more information, see the [schema for this report](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json). The JSON report file can be downloaded from the CI pipelines page, or the -pipelines tab on merge requests by [setting `artifacts: paths`](../../../ci/pipelines/job_artifacts.md#defining-artifacts-in-gitlab-ciyml) to `gl-sast-report.json`. For more information see [Downloading artifacts](../../../ci/pipelines/job_artifacts.md). +pipelines tab on merge requests by [setting `artifacts: paths`](../../../ci/yaml/README.md#artifactspaths) to `gl-sast-report.json`. For more information see [Downloading artifacts](../../../ci/pipelines/job_artifacts.md). Here's an example SAST report: diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md index f66fc0a0f6a..9c0eb571f66 100644 --- a/doc/user/project/integrations/hipchat.md +++ b/doc/user/project/integrations/hipchat.md @@ -4,7 +4,12 @@ group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Atlassian HipChat **(FREE)** +# Atlassian HipChat (Deprecated) **(FREE)** + +As of [GitLab +13.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57434), the +HipChat integration will not send any notifications to HipChat. This +integration is also deprecated. GitLab provides a way to send HipChat notifications upon a number of events, such as when a user pushes code, creates a branch or tag, adds a comment, and diff --git a/doc/user/project/merge_requests/accessibility_testing.md b/doc/user/project/merge_requests/accessibility_testing.md index c518113d3dd..09770bd447d 100644 --- a/doc/user/project/merge_requests/accessibility_testing.md +++ b/doc/user/project/merge_requests/accessibility_testing.md @@ -57,7 +57,7 @@ include: creates an `a11y` job in your CI/CD pipeline, runs Pa11y against the web pages defined in `a11y_urls`, and builds an HTML report for each. -The report for each URL is saved as an artifact that can be [viewed directly in your browser](../../../ci/pipelines/job_artifacts.md#browsing-artifacts). +The report for each URL is saved as an artifact that can be [viewed directly in your browser](../../../ci/pipelines/job_artifacts.md#download-job-artifacts). A single `gl-accessibility.json` artifact is created and saved along with the individual HTML reports. It includes report data for all URLs scanned. diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md index 1733284d812..9575f39b244 100644 --- a/doc/user/project/merge_requests/code_quality.md +++ b/doc/user/project/merge_requests/code_quality.md @@ -355,7 +355,7 @@ After the Code Quality job completes: The Code Quality widget in the merge request compares the reports from the base and head of the branch, then lists any violations that are resolved or created when the branch is merged. - The full JSON report is available as a - [downloadable artifact](../../../ci/pipelines/job_artifacts.md#downloading-artifacts) + [downloadable artifact](../../../ci/pipelines/job_artifacts.md#download-job-artifacts) for the `code_quality` job. - The full list of code quality violations generated by a pipeline is shown in the Code Quality tab of the Pipeline Details page. **(PREMIUM)** diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index 3e71f543d3d..277238bfc98 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -290,7 +290,7 @@ This problem most likely results from a missing `index.html` file in the public a 404 is encountered, confirm that the public directory contains an `index.html` file. If the file contains a different name such as `test.html`, the Pages site can still be accessed, but the full path would be needed. For example: `https//group-name.pages.example.com/project-name/test.html`. -The contents of the public directory can be confirmed by [browsing the artifacts](../../../ci/pipelines/job_artifacts.md#browsing-artifacts) from the latest pipeline. +The contents of the public directory can be confirmed by [browsing the artifacts](../../../ci/pipelines/job_artifacts.md#download-job-artifacts) from the latest pipeline. Files listed under the public directory can be accessed through the Pages URL for the project. diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index 3d9955e8f1a..339c0e779f9 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -70,7 +70,7 @@ module API optional :variables, Array, desc: 'Array of variables available in the pipeline' end post ':id/pipeline' do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42124') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20711') authorize! :create_pipeline, user_project diff --git a/lib/api/groups.rb b/lib/api/groups.rb index d0dc1273ed1..ec097e3e3d9 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -112,7 +112,7 @@ module API end def delete_group(group) - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/22226') destroy_conditionally!(group) do |group| ::Groups::DestroyService.new(group, current_user).async_execute end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index e14828ae7df..aff9b097828 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -242,7 +242,7 @@ module API use :issue_params end post ':id/issues' do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42320') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20773') check_rate_limit! :issues_create, [current_user] @@ -288,7 +288,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord put ':id/issues/:issue_iid' do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42322') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20775') issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) authorize! :update_issue, issue @@ -346,7 +346,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord post ':id/issues/:issue_iid/move' do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42323') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20776') issue = user_project.issues.find_by(iid: params[:issue_iid]) not_found!('Issue') unless issue diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index d3bcef9c7c4..44fe167e05c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -207,7 +207,7 @@ module API use :optional_params end post ":id/merge_requests" do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42316') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20770') authorize! :create_merge_request_from, user_project @@ -416,7 +416,7 @@ module API at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of) end put ':id/merge_requests/:merge_request_iid' do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42318') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20772') merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request) @@ -445,7 +445,7 @@ module API optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' end put ':id/merge_requests/:merge_request_iid/merge' do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42317') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/4796') merge_request = find_project_merge_request(params[:merge_request_iid]) diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index c7917504f68..5f3a574eeee 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -67,7 +67,7 @@ module API check_rate_limit! :project_import, [current_user, :project_import] - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42437') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20823') validate_file! diff --git a/lib/api/projects.rb b/lib/api/projects.rb index a18fceed2bb..714012b9e76 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -310,7 +310,7 @@ module API optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the fork' end post ':id/fork', feature_category: :source_code_management do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42284') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20759') not_found! unless can?(current_user, :fork_project, user_project) diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 3988a995450..84c51e5aeac 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -21,7 +21,7 @@ module API optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do - Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab-foss/issues/42283') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20758') forbidden! if gitlab_pipeline_hook_request? diff --git a/lib/backup/repositories.rb b/lib/backup/repositories.rb index 79b7b2c61f2..627bb44331b 100644 --- a/lib/backup/repositories.rb +++ b/lib/backup/repositories.rb @@ -249,7 +249,7 @@ module Backup progress.puts " * #{display_repo_path} ... " if repository.empty? - progress.puts " * #{display_repo_path} ... " + "[SKIPPED]".color(:cyan) + progress.puts " * #{display_repo_path} ... " + "[EMPTY] [SKIPPED]".color(:cyan) return end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a7df3acceda..94d08ef446b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19977,6 +19977,9 @@ msgstr "" msgid "More Information" msgstr "" +msgid "More Information." +msgstr "" + msgid "More Slack commands" msgstr "" @@ -34256,9 +34259,6 @@ msgstr "" msgid "WikiClone|Start Gollum and edit locally" msgstr "" -msgid "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title." -msgstr "" - msgid "WikiEdit|There is already a page with the same title in that path." msgstr "" @@ -34331,18 +34331,6 @@ msgstr "" msgid "WikiHistoricalPage|most recent version" msgstr "" -msgid "WikiMarkdownDocs|More examples are in the %{docs_link}" -msgstr "" - -msgid "WikiMarkdownDocs|documentation" -msgstr "" - -msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}" -msgstr "" - -msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories." -msgstr "" - msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?" msgstr "" @@ -34355,10 +34343,37 @@ msgstr "" msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs." msgstr "" -msgid "WikiPageCreate|Create %{pageTitle}" +msgid "WikiPage|Commit message" msgstr "" -msgid "WikiPageEdit|Update %{pageTitle}" +msgid "WikiPage|Content" +msgstr "" + +msgid "WikiPage|Create %{pageTitle}" +msgstr "" + +msgid "WikiPage|Create page" +msgstr "" + +msgid "WikiPage|Format" +msgstr "" + +msgid "WikiPage|Page title" +msgstr "" + +msgid "WikiPage|Tip: You can move this page by adding the path to the beginning of the title." +msgstr "" + +msgid "WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories." +msgstr "" + +msgid "WikiPage|Title" +msgstr "" + +msgid "WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}." +msgstr "" + +msgid "WikiPage|Update %{pageTitle}" msgstr "" msgid "WikiPage|Write your content or drag files here…" @@ -34370,9 +34385,6 @@ msgstr "" msgid "Wiki|Create New Page" msgstr "" -msgid "Wiki|Create page" -msgstr "" - msgid "Wiki|Created date" msgstr "" @@ -34385,9 +34397,6 @@ msgstr "" msgid "Wiki|Page history" msgstr "" -msgid "Wiki|Page title" -msgstr "" - msgid "Wiki|Page version" msgstr "" diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb index e24b1b67af1..bb22b7da003 100644 --- a/qa/qa/page/component/wiki_page_form.rb +++ b/qa/qa/page/component/wiki_page_form.rb @@ -9,12 +9,11 @@ module QA def self.included(base) super - base.view 'app/views/shared/wikis/_form.html.haml' do + base.view 'app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue' do element :wiki_title_textbox element :wiki_content_textarea element :wiki_message_textbox - element :save_changes_button - element :create_page_button + element :wiki_submit_button end base.view 'app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue' do @@ -34,12 +33,8 @@ module QA fill_element(:wiki_message_textbox, message) end - def click_save_changes - click_element(:save_changes_button) - end - - def click_create_page - click_element(:create_page_button) + def click_submit + click_element(:wiki_submit_button) end def delete_page diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_creation_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_creation_spec.rb index 6044c87d24e..70959dd0200 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_creation_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_creation_spec.rb @@ -27,7 +27,7 @@ module QA edit.set_message commit_message end - Page::Project::Wiki::Edit.perform(&:click_create_page) + Page::Project::Wiki::Edit.perform(&:click_submit) Page::Project::Wiki::Show.perform do |wiki| expect(wiki).to have_title new_wiki_title @@ -46,7 +46,7 @@ module QA edit.set_message commit_message end - Page::Project::Wiki::Edit.perform(&:click_create_page) + Page::Project::Wiki::Edit.perform(&:click_submit) Page::Project::Wiki::Show.perform do |wiki| expect(wiki).to have_title new_wiki_title diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_manipulation_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_manipulation_spec.rb index 30a91c2e254..9a6d7d08e7b 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_manipulation_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_content_manipulation_spec.rb @@ -25,7 +25,7 @@ module QA edit.set_message commit_message end - Page::Project::Wiki::Edit.perform(&:click_save_changes) + Page::Project::Wiki::Edit.perform(&:click_submit) Page::Project::Wiki::Show.perform do |wiki| expect(wiki).to have_title new_wiki_title diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb index 4f1d9ac1696..1a46322d283 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb @@ -20,7 +20,7 @@ module QA edit.set_message('changing the path of the home page') end - Page::Project::Wiki::Edit.perform(&:click_save_changes) + Page::Project::Wiki::Edit.perform(&:click_submit) Page::Project::Wiki::Show.perform do |wiki| expect(wiki).to have_directory('a') diff --git a/scripts/perf/query_limiting_report.rb b/scripts/perf/query_limiting_report.rb new file mode 100755 index 00000000000..2f263eeb567 --- /dev/null +++ b/scripts/perf/query_limiting_report.rb @@ -0,0 +1,167 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +#### +# Prints a report which helps reconcile occurrences of the `QueryLimiting.disable(ISSUE_LINK)` +# allowlist block against the corresponding open issues. +# +# If everything is consistent, the script should ideally not report any issues or code lines, +# other than possibly remaining "calls with no issue iid" which use variables/etc. +# +# - See https://gitlab.com/gitlab-org/gitlab/-/issues/325640 +# - See https://gitlab.com/groups/gitlab-org/-/epics/5670 + +require 'rubygems' +require 'gitlab' +require 'optparse' + +class QueryLimitingReport + GITLAB_PROJECT_ID = 278964 # gitlab-org/gitlab project + ISSUES_SEARCH_LABEL = 'querylimiting-disable' + CODE_LINES_SEARCH_STRING = 'QueryLimiting.disable' + PAGINATION_LIMIT = 500 + + DEFAULT_OPTIONS = { + api_token: ENV['API_TOKEN'] + }.freeze + + def initialize(options) + @options = options + + Gitlab.configure do |config| + config.endpoint = 'https://gitlab.com/api/v4' + config.private_token = options.fetch(:api_token) + end + end + + def execute + # PLAN: + # Read all issues matching criteria and extract array of issue iids + # Find all code references and extract issue iids + # Print list of all issues without code references + # Print list of all code references issue iids that don't have search label + # Print list of all code references with no issue iids (i.e. dynamic or variable argument) + + total_issues = find_issues_by_label(ISSUES_SEARCH_LABEL) + issues = total_issues.select { |issue| issue[:state] == 'opened' } + code_lines = find_code_lines + + code_lines_grouped = code_lines.group_by { |code_line| code_line[:has_issue_iid] } + code_lines_without_issue_iid = code_lines_grouped[false] + code_lines_with_issue_iid = code_lines_grouped[true] + + all_issue_iids_in_code_lines = code_lines_with_issue_iid.map { |line| line[:issue_iid] } + + issues_without_code_references = issues.reject do |issue| + all_issue_iids_in_code_lines.include?(issue[:iid]) + end + + all_issue_iids = issues.map { |issue| issue[:iid] } + code_lines_with_missing_issues = code_lines_with_issue_iid.reject do |code_line| + all_issue_iids.include?(code_line[:issue_iid]) + end + + puts "\n\n\nREPORT:" + + puts "\n\nFound #{total_issues.length} total issues with '#{ISSUES_SEARCH_LABEL}' search label, #{issues.length} are still opened..." + puts "\n\nFound #{code_lines.length} total occurrences of '#{CODE_LINES_SEARCH_STRING}' in code..." + + puts "\n" + '-' * 80 + + puts "\n\nIssues without any '#{CODE_LINES_SEARCH_STRING}' code references (#{issues_without_code_references.length} total):" + pp issues_without_code_references + + puts "\n" + '-' * 80 + + puts "\n\n'#{CODE_LINES_SEARCH_STRING}' calls with references to an issue which doesn't have '#{ISSUES_SEARCH_LABEL}' search label (#{code_lines_with_missing_issues.length} total):" + pp code_lines_with_missing_issues + + puts "\n" + '-' * 80 + + puts "\n\n'#{CODE_LINES_SEARCH_STRING}' calls with no issue iid (#{code_lines_without_issue_iid&.length || 0} total):" + pp code_lines_without_issue_iid + end + + private + + attr_reader :options + + def find_issues_by_label(label) + issues = [] + + puts("Finding issues by label #{label}...") + paginated_issues = Gitlab.issues(GITLAB_PROJECT_ID, 'labels' => label) + paginated_issues.paginate_with_limit(PAGINATION_LIMIT) do |item| + item_hash = item.to_hash + + issue_iid = item_hash.fetch('iid') + issue = { + iid: issue_iid, + state: item_hash.fetch('state'), + title: item_hash.fetch('title'), + issue_url: "https://gitlab.com/gitlab-org/gitlab/issues/#{issue_iid}" + } + + issues << issue + end + + issues + end + + def find_code_lines + code_lines = [] + + puts("Finding code lines...") + paginated_blobs = Gitlab.search_in_project(GITLAB_PROJECT_ID, 'blobs', CODE_LINES_SEARCH_STRING) + paginated_blobs.paginate_with_limit(PAGINATION_LIMIT) do |item| + item_hash = item.to_hash + + filename = item_hash.fetch('filename') + next if filename !~ /\.rb\Z/ + + file_contents = Gitlab.file_contents(GITLAB_PROJECT_ID, filename) + file_lines = file_contents.split("\n") + + file_lines.each_index do |index| + line = file_lines[index] + if line =~ /#{CODE_LINES_SEARCH_STRING}/ + issue_iid = line.slice(%r{issues/(\d+)\D}, 1) + line_number = index + 1 + code_line = { + file_location: "#{filename}:#{line_number}", + filename: filename, + line_number: line_number, + line: line, + issue_iid: issue_iid.to_i, + has_issue_iid: !issue_iid.nil? + } + code_lines << code_line + end + end + end + + code_lines.sort_by! { |line| "#{line[:filename]}-#{line[:line_number].to_s.rjust(4, '0')}" } + code_lines.map do |line| + line.delete(:filename) + line.delete(:line_number) + line + end + end +end + +if $0 == __FILE__ + options = QueryLimitingReport::DEFAULT_OPTIONS.dup + + OptionParser.new do |opts| + opts.on("-t", "--api-token API_TOKEN", String, "A value API token with the `read_api` scope. Can be set as an env variable 'API_TOKEN'.") do |value| + options[:api_token] = value + end + + opts.on("-h", "--help", "Prints this help") do + puts opts + exit + end + end.parse! + + QueryLimitingReport.new(options).execute +end diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index ee156bdcab4..cc04b3b645b 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -42,7 +42,7 @@ RSpec.describe 'Contributions Calendar', :js do "#{contributions} #{'contribution'.pluralize(contributions)}" end - "#{get_cell_color_selector(contributions)}[title='#{contribution_text}
#{date}']" + "#{get_cell_color_selector(contributions)}[title='#{contribution_text}
#{date}']" end def push_code_contribution diff --git a/spec/frontend/pages/shared/wikis/wiki_alert_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js similarity index 100% rename from spec/frontend/pages/shared/wikis/wiki_alert_spec.js rename to spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js new file mode 100644 index 00000000000..b18bad1ad9b --- /dev/null +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -0,0 +1,222 @@ +import { mount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue'; + +describe('WikiForm', () => { + let wrapper; + + const findForm = () => wrapper.find('form'); + const findTitle = () => wrapper.find('#wiki_title'); + const findFormat = () => wrapper.find('#wiki_format'); + const findContent = () => wrapper.find('#wiki_content'); + const findMessage = () => wrapper.find('#wiki_message'); + const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button'); + const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button'); + const findTitleHelpLink = () => wrapper.findByTestId('wiki-title-help-link'); + const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link'); + + const pageInfoNew = { + persisted: false, + uploadsPath: '/project/path/-/wikis/attachments', + wikiPath: '/project/path/-/wikis', + helpPath: '/help/user/project/wiki/index', + markdownHelpPath: '/help/user/markdown', + markdownPreviewPath: '/project/path/-/wikis/.md/preview-markdown', + createPath: '/project/path/-/wikis/new', + }; + + const pageInfoPersisted = { + ...pageInfoNew, + persisted: true, + + title: 'My page', + content: 'My page content', + format: 'markdown', + path: '/project/path/-/wikis/home', + }; + + function createWrapper(persisted = false, pageInfo = {}) { + wrapper = extendedWrapper( + mount( + WikiForm, + { + provide: { + formatOptions: { + Markdown: 'markdown', + RDoc: 'rdoc', + AsciiDoc: 'asciidoc', + Org: 'org', + }, + pageInfo: { + ...(persisted ? pageInfoPersisted : pageInfoNew), + ...pageInfo, + }, + }, + }, + { attachToDocument: true }, + ), + ); + + jest.spyOn(wrapper.vm, 'onBeforeUnload'); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it.each` + title | persisted | message + ${'my page'} | ${false} | ${'Create my page'} + ${'my-page'} | ${false} | ${'Create my page'} + ${'somedir/my-page'} | ${false} | ${'Create somedir/my page'} + ${'my-page'} | ${true} | ${'Update my page'} + `( + 'updates the commit message to $message when title is $title and persisted=$persisted', + async ({ title, message, persisted }) => { + createWrapper(persisted); + + findTitle().setValue(title); + + await wrapper.vm.$nextTick(); + + expect(findMessage().element.value).toBe(message); + }, + ); + + it('sets the commit message to "Update My page" when the page first loads when persisted', async () => { + createWrapper(true); + + await wrapper.vm.$nextTick(); + + expect(findMessage().element.value).toBe('Update My page'); + }); + + it.each` + value | text + ${'markdown'} | ${'[Link Title](page-slug)'} + ${'rdoc'} | ${'{Link title}[link:page-slug]'} + ${'asciidoc'} | ${'link:page-slug[Link title]'} + ${'org'} | ${'[[page-slug]]'} + `('updates the link help message when format=$value is selected', async ({ value, text }) => { + createWrapper(); + + findFormat().find(`option[value=${value}]`).setSelected(); + + await wrapper.vm.$nextTick(); + + expect(wrapper.text()).toContain(text); + }); + + it('starts with no unload warning', async () => { + createWrapper(); + + await wrapper.vm.$nextTick(); + + window.dispatchEvent(new Event('beforeunload')); + + expect(wrapper.vm.onBeforeUnload).not.toHaveBeenCalled(); + }); + + it.each` + persisted | titleHelpText | titleHelpLink + ${true} | ${'You can move this page by adding the path to the beginning of the title.'} | ${'/help/user/project/wiki/index#moving-a-wiki-page'} + ${false} | ${'You can specify the full path for the new file. We will automatically create any missing directories.'} | ${'/help/user/project/wiki/index#creating-a-new-wiki-page'} + `( + 'shows appropriate title help text and help link for when persisted=$persisted', + async ({ persisted, titleHelpLink, titleHelpText }) => { + createWrapper(persisted); + + await wrapper.vm.$nextTick(); + + expect(wrapper.text()).toContain(titleHelpText); + expect(findTitleHelpLink().attributes().href).toEqual(titleHelpLink); + }, + ); + + it('shows correct link for wiki specific markdown docs', async () => { + createWrapper(); + + await wrapper.vm.$nextTick(); + + expect(findMarkdownHelpLink().attributes().href).toEqual( + '/help/user/markdown#wiki-specific-markdown', + ); + }); + + describe('when wiki content is updated', () => { + beforeEach(() => { + createWrapper(); + + const input = findContent(); + input.setValue('Lorem ipsum dolar sit!'); + input.element.dispatchEvent(new Event('input')); + + return wrapper.vm.$nextTick(); + }); + + it('sets before unload warning', () => { + window.dispatchEvent(new Event('beforeunload')); + + expect(wrapper.vm.onBeforeUnload).toHaveBeenCalled(); + }); + + it('when form submitted, unsets before unload warning', async () => { + findForm().element.dispatchEvent(new Event('submit')); + + await wrapper.vm.$nextTick(); + + window.dispatchEvent(new Event('beforeunload')); + + expect(wrapper.vm.onBeforeUnload).not.toHaveBeenCalled(); + }); + }); + + describe('submit button state', () => { + it.each` + title | content | buttonState | disabledAttr + ${'something'} | ${'something'} | ${'enabled'} | ${undefined} + ${''} | ${'something'} | ${'disabled'} | ${'disabled'} + ${'something'} | ${''} | ${'disabled'} | ${'disabled'} + ${''} | ${''} | ${'disabled'} | ${'disabled'} + ${' '} | ${' '} | ${'disabled'} | ${'disabled'} + `( + "when title='$title', content='$content', then the button is $buttonState'", + async ({ title, content, disabledAttr }) => { + createWrapper(); + + findTitle().setValue(title); + findContent().setValue(content); + + await wrapper.vm.$nextTick(); + + expect(findSubmitButton().attributes().disabled).toBe(disabledAttr); + }, + ); + + it.each` + persisted | buttonLabel + ${true} | ${'Save changes'} + ${false} | ${'Create page'} + `('when persisted=$persisted, label is set to $buttonLabel', ({ persisted, buttonLabel }) => { + createWrapper(persisted); + + expect(findSubmitButton().text()).toBe(buttonLabel); + }); + }); + + describe('cancel button state', () => { + it.each` + persisted | redirectLink + ${false} | ${'/project/path/-/wikis'} + ${true} | ${'/project/path/-/wikis/home'} + `( + 'when persisted=$persisted, redirects the user to appropriate path', + ({ persisted, redirectLink }) => { + createWrapper(persisted); + + expect(findCancelButton().attributes().href).toEqual(redirectLink); + }, + ); + }); +}); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap index cef5f8cc528..885f42cc065 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap @@ -25,6 +25,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
{ - const editFormHtmlFixture = (args) => `
- - - - - {Link title}[link:page-slug] - - -
- `; - - let wikis; - let titleInput; - let contentInput; - let messageInput; - let changeFormatSelect; - let linkExample; - - const findBeforeUnloadWarning = () => window.onbeforeunload?.(); - const findForm = () => document.querySelector('.wiki-form'); - const findSubmitButton = () => document.querySelector('.js-wiki-btn-submit'); - - describe('when the wiki page is being created', () => { - const formHtmlFixture = editFormHtmlFixture({ newPage: true }); - - beforeEach(() => { - setHTMLFixture(formHtmlFixture); - - titleInput = document.getElementById('wiki_title'); - messageInput = document.getElementById('wiki_message'); - changeFormatSelect = document.querySelector('#wiki_format'); - linkExample = document.querySelector('.js-markup-link-example'); - wikis = new Wikis(); - }); - - it('binds an event listener to the title input', () => { - wikis.handleWikiTitleChange = jest.fn(); - - titleInput.dispatchEvent(new Event('keyup')); - - expect(wikis.handleWikiTitleChange).toHaveBeenCalled(); - }); - - it('sets the commit message when title changes', () => { - titleInput.value = 'My title'; - messageInput.value = ''; - - titleInput.dispatchEvent(new Event('keyup')); - - expect(messageInput.value).toEqual('Create My title'); - }); - - it('replaces hyphens with spaces', () => { - titleInput.value = 'my-hyphenated-title'; - titleInput.dispatchEvent(new Event('keyup')); - - expect(messageInput.value).toEqual('Create my hyphenated title'); - }); - }); - - describe('when the wiki page is being updated', () => { - const formHtmlFixture = editFormHtmlFixture({ newPage: false }); - - beforeEach(() => { - setHTMLFixture(formHtmlFixture); - - titleInput = document.getElementById('wiki_title'); - messageInput = document.getElementById('wiki_message'); - wikis = new Wikis(); - }); - - it('sets the commit message when title changes, prefixing with "Update"', () => { - titleInput.value = 'My title'; - messageInput.value = ''; - - titleInput.dispatchEvent(new Event('keyup')); - - expect(messageInput.value).toEqual('Update My title'); - }); - - it.each` - value | text - ${'markdown'} | ${'[Link Title](page-slug)'} - ${'rdoc'} | ${'{Link title}[link:page-slug]'} - ${'asciidoc'} | ${'link:page-slug[Link title]'} - ${'org'} | ${'[[page-slug]]'} - `('updates a message when value=$value is selected', ({ value, text }) => { - changeFormatSelect.value = value; - changeFormatSelect.dispatchEvent(new Event('change')); - - expect(linkExample.innerHTML).toBe(text); - }); - - it('starts with no unload warning', () => { - expect(findBeforeUnloadWarning()).toBeUndefined(); - }); - - describe('when wiki content is updated', () => { - beforeEach(() => { - contentInput = document.getElementById('wiki_content'); - contentInput.value = 'Lorem ipsum dolar sit!'; - contentInput.dispatchEvent(new Event('input')); - }); - - it('sets before unload warning', () => { - expect(findBeforeUnloadWarning()).toBe(''); - }); - - it('when form submitted, unsets before unload warning', () => { - findForm().dispatchEvent(new Event('submit')); - expect(findBeforeUnloadWarning()).toBeUndefined(); - }); - }); - }); - - describe('submit button state', () => { - beforeEach(() => { - setHTMLFixture(editFormHtmlFixture({ newPage: true })); - - titleInput = document.getElementById('wiki_title'); - contentInput = document.getElementById('wiki_content'); - - wikis = new Wikis(); - }); - - it.each` - title | text | buttonState | disabledAttr - ${'something'} | ${'something'} | ${'enabled'} | ${null} - ${''} | ${'something'} | ${'disabled'} | ${'true'} - ${'something'} | ${''} | ${'disabled'} | ${'true'} - ${''} | ${''} | ${'disabled'} | ${'true'} - ${' '} | ${' '} | ${'disabled'} | ${'true'} - `( - "when title='$title', content='$content', then, buttonState='$buttonState'", - ({ title, text, disabledAttr }) => { - titleInput.value = title; - titleInput.dispatchEvent(new Event('keyup')); - - contentInput.value = text; - contentInput.dispatchEvent(new Event('input')); - - expect(findSubmitButton().getAttribute('disabled')).toBe(disabledAttr); - }, - ); - }); - describe('trackPageView', () => { const trackingPage = 'projects:wikis:show'; const trackingContext = { foo: 'bar' }; diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 667e6cc85ab..82a4cde752b 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -49,307 +49,8 @@ RSpec.describe HipchatService do WebMock.stub_request(:post, api_url) end - it 'tests and return errors' do - allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room') - result = hipchat.test(push_sample_data) - - expect(result[:success]).to be_falsey - expect(result[:result].to_s).to eq('no such room') - end - - it 'uses v1 if version is provided' do - allow(hipchat).to receive(:api_version).and_return('v1') - expect(HipChat::Client).to receive(:new).with( - token, - api_version: 'v1', - server_url: server_url - ).and_return(double(:hipchat_service).as_null_object) - hipchat.execute(push_sample_data) - end - - it 'uses v2 as the version when nothing is provided' do - allow(hipchat).to receive(:api_version).and_return('') - expect(HipChat::Client).to receive(:new).with( - token, - api_version: 'v2', - server_url: server_url - ).and_return(double(:hipchat_service).as_null_object) - hipchat.execute(push_sample_data) - end - - context 'push events' do - it "calls Hipchat API for push events" do - hipchat.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a push message" do - message = hipchat.send(:create_push_message, push_sample_data) - - push_sample_data[:object_attributes] - branch = push_sample_data[:ref].gsub('refs/heads/', '') - expect(message).to include("#{user.name} pushed to branch " \ - "#{branch} of " \ - "#{project_name}") - end - end - - context 'tag_push events' do - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build( - project: project, - user: user, - oldrev: Gitlab::Git::BLANK_SHA, - newrev: '1' * 40, - ref: 'refs/tags/test') - end - - it "calls Hipchat API for tag push events" do - hipchat.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a tag push message" do - message = hipchat.send(:create_push_message, push_sample_data) - - push_sample_data[:object_attributes] - expect(message).to eq("#{user.name} pushed new tag " \ - "test to " \ - "#{project_name}\n") - end - end - - context 'issue events' do - let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') } - let(:issue_service) { Issues::CreateService.new(project, user) } - let(:issues_sample_data) { issue_service.hook_data(issue, 'open') } - - it "calls Hipchat API for issue events" do - hipchat.execute(issues_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates an issue message" do - message = hipchat.send(:create_issue_message, issues_sample_data) - - obj_attr = issues_sample_data[:object_attributes] - expect(message).to eq("#{user.name} opened " \ - "issue ##{obj_attr["iid"]} in " \ - "#{project_name}: " \ - "Awesome issue" \ - "
please fix
") - end - end - - context 'merge request events' do - let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) } - let(:merge_service) { MergeRequests::CreateService.new(project, user) } - let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } - - it "calls Hipchat API for merge requests events" do - hipchat.execute(merge_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a merge request message" do - message = hipchat.send(:create_merge_request_message, - merge_sample_data) - - obj_attr = merge_sample_data[:object_attributes] - expect(message).to eq("#{user.name} opened " \ - "merge request !#{obj_attr["iid"]} in " \ - "#{project_name}: " \ - "Awesome merge request" \ - "
please fix
") - end - end - - context "Note events" do - let(:user) { create(:user) } - let(:project) { create(:project, :repository, creator: user) } - - context 'when commit comment event triggered' do - let(:commit_note) do - create(:note_on_commit, author: user, project: project, - commit_id: project.repository.commit.id, - note: 'a comment on a commit') - end - - it "calls Hipchat API for commit comment events" do - data = Gitlab::DataBuilder::Note.build(commit_note, user) - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - commit_id = Commit.truncate_sha(data[:commit][:id]) - title = hipchat.send(:format_title, data[:commit][:message]) - - expect(message).to eq("#{user.name} commented on " \ - "commit #{commit_id} in " \ - "#{project_name}: " \ - "#{title}" \ - "
a comment on a commit
") - end - end - - context 'when merge request comment event triggered' do - let(:merge_request) do - create(:merge_request, source_project: project, - target_project: project) - end - - let(:merge_request_note) do - create(:note_on_merge_request, noteable: merge_request, - project: project, - note: "merge request **note**") - end - - it "calls Hipchat API for merge request comment events" do - data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - merge_id = data[:merge_request]['iid'] - title = data[:merge_request]['title'] - - expect(message).to eq("#{user.name} commented on " \ - "merge request !#{merge_id} in " \ - "#{project_name}: " \ - "#{title}" \ - "
merge request note
") - end - end - - context 'when issue comment event triggered' do - let(:issue) { create(:issue, project: project) } - let(:issue_note) do - create(:note_on_issue, noteable: issue, project: project, - note: "issue **note**") - end - - it "calls Hipchat API for issue comment events" do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - hipchat.execute(data) - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - issue_id = data[:issue]['iid'] - title = data[:issue]['title'] - - expect(message).to eq("#{user.name} commented on " \ - "issue ##{issue_id} in " \ - "#{project_name}: " \ - "#{title}" \ - "
issue note
") - end - - context 'with confidential issue' do - before do - issue.update!(confidential: true) - end - - it 'calls Hipchat API with issue comment' do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - hipchat.execute(data) - - message = hipchat.send(:create_message, data) - - expect(message).to include("
issue note
") - end - end - end - - context 'when snippet comment event triggered' do - let(:snippet) { create(:project_snippet, project: project) } - let(:snippet_note) do - create(:note_on_project_snippet, noteable: snippet, - project: project, - note: "snippet note") - end - - it "calls Hipchat API for snippet comment events" do - data = Gitlab::DataBuilder::Note.build(snippet_note, user) - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - snippet_id = data[:snippet]['id'] - title = data[:snippet]['title'] - - expect(message).to eq("#{user.name} commented on " \ - "snippet ##{snippet_id} in " \ - "#{project_name}: " \ - "#{title}" \ - "
snippet note
") - end - end - end - - context 'pipeline events' do - let(:pipeline) { create(:ci_empty_pipeline, user: project.owner) } - let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } - - context 'for failed' do - before do - pipeline.drop - end - - it "calls Hipchat API" do - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a build message" do - message = hipchat.__send__(:create_pipeline_message, data) - - project_url = project.web_url - project_name = project.full_name.gsub(/\s/, '') - pipeline_attributes = data[:object_attributes] - ref = pipeline_attributes[:ref] - ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' - duration = pipeline_attributes[:duration] - user_name = data[:user][:name] - - expect(message).to eq("#{project_name}: " \ - "Pipeline ##{pipeline.id} " \ - "of #{ref} #{ref_type} " \ - "by #{user_name} failed in #{duration} second(s)") - end - end - - context 'for succeeded' do - before do - pipeline.succeed - end - - it "calls Hipchat API" do - hipchat.notify_only_broken_pipelines = false - hipchat.execute(data) - expect(WebMock).to have_requested(:post, api_url).once - end - - it "notifies only broken" do - hipchat.notify_only_broken_pipelines = true - hipchat.execute(data) - expect(WebMock).not_to have_requested(:post, api_url).once - end - end + it 'does nothing' do + expect { hipchat.execute(push_sample_data) }.not_to raise_error end describe "#message_options" do @@ -388,22 +89,4 @@ RSpec.describe HipchatService do end end end - - context 'with UrlBlocker' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:hipchat) { create(:hipchat_service, project: project, properties: { room: 'test' }) } - let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - - describe '#execute' do - before do - hipchat.server = 'http://localhost:9123' - end - - it 'raises UrlBlocker for localhost' do - expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original - expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError) - end - end - end end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index 2f8ebd0d264..8a6d5d88ca6 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -240,7 +240,7 @@ RSpec.shared_examples 'User creates wiki page' do end end - it "shows the emoji autocompletion dropdown" do + it "shows the emoji autocompletion dropdown", :js do click_link("New page") page.within(".wiki-form") do diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb index a22d98f20c4..1a981f42086 100644 --- a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb @@ -38,19 +38,19 @@ RSpec.shared_examples 'User previews wiki changes' do end end - context "when there are no spaces or hyphens in the page name" do + context "when there are no spaces or hyphens in the page name", :js do let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a/b/c/d', content: page_content) } it_behaves_like 'rewrites relative links' end - context "when there are spaces in the page name" do + context "when there are spaces in the page name", :js do let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a page/b page/c page/d page', content: page_content) } it_behaves_like 'rewrites relative links' end - context "when there are hyphens in the page name" do + context "when there are hyphens in the page name", :js do let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a-page/b-page/c-page/d-page', content: page_content) } it_behaves_like 'rewrites relative links' diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 1e325535e81..d185e9dd81c 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -11,7 +11,7 @@ RSpec.shared_examples 'User updates wiki page' do sign_in(user) end - context 'when wiki is empty' do + context 'when wiki is empty', :js do before do |example| visit(wiki_path(wiki)) @@ -57,7 +57,7 @@ RSpec.shared_examples 'User updates wiki page' do it_behaves_like 'wiki file attachments' end - context 'when wiki is not empty' do + context 'when wiki is not empty', :js do let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') } before do @@ -147,7 +147,7 @@ RSpec.shared_examples 'User updates wiki page' do it_behaves_like 'wiki file attachments' end - context 'when the page is in a subdir' do + context 'when the page is in a subdir', :js do let(:page_name) { 'page_name' } let(:page_dir) { "foo/bar/#{page_name}" } let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: page_dir, content: 'Home page') } diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb index 14180d503df..d5d8884b55a 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb @@ -24,7 +24,7 @@ RSpec.shared_examples 'User views empty wiki' do # This mirrors the logic in: # - app/views/shared/empty_states/_wikis.html.haml # - WikiHelper#wiki_empty_state_messages - it 'shows the empty state message with the expected elements' do + it 'shows the empty state message with the expected elements', :js do visit wiki_path(wiki) if writable