diff --git a/.gitlab/issue_templates/Acceptance_Testing.md b/.gitlab/issue_templates/Acceptance_Testing.md new file mode 100644 index 0000000000000000000000000000000000000000..f1fbb96ce6115a9a6db5708081b97078f801f73d --- /dev/null +++ b/.gitlab/issue_templates/Acceptance_Testing.md @@ -0,0 +1,100 @@ +## Details +- **Feature Toggle Name**: `FEATURE_NAME` +- **Required GitLab Version**: `vX.X` + +-------------------------------------------------------------------------------- + +## 1. Preparation + +- [ ] **Controllers and workers**: + 1. Please link to dashboards of the workers, and the controllers and actions that can be impacted + 2. ... + 3. ... + +## 2. Development Trial + +#### Check Dev Server Versions +- [ ] GitLab: https://dev.gitlab.org/help + +#### Enable on `dev.gitlab.org`: +- [ ] `/chatops feature set FEATURE_NAME true --dev` in [`#dev-gitlab`](https://gitlab.slack.com/messages/C6WQ87MU3) + +Then leave running while monitoring and performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) + +## 2. Staging Trial + +#### Check Staging Server Versions +- [ ] GitLab: https://staging.gitlab.com/help + +#### Enable on `staging.gitlab.com` +- [ ] `/chatops run feature set FEATURE_NAME true --staging` in [`#development`](https://gitlab.slack.com/messages/C02PF508L/) + +Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 4. Production Server Version Check + +- [ ] GitLab: https://gitlab.com/help + +## 5. Initial Impact Check + +- [ ] Enable for a subset of users, when using percentage gates: 1%. + +Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 6. Low Impact Check + +- [ ] Enable for a bigger subset of users, when using percentage gates: 10%. + +Then leave running while monitoring for at least **30 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 7. Mid Impact Trial + +- [ ] Enable for a big subset of users, when using percentage gates: 50%. + +Then leave running while monitoring for at least **12 hours** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 8. Full Impact Trial + +- [ ] Enable for all users: `/chatops run feature set FEATURE_NAME true + +Then leave running while monitoring for at least **1 week**. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) + +#### Success? + +- [ ] Remove the feature gate from the code, and close this issue with that MR. diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 831446cbd27a6de403344b21c9fa93a25357f43d..09b254e90c61ed28bb68a54752cf04f6a736a7d3 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -5.1.0 +6.0.0 diff --git a/Gemfile.lock b/Gemfile.lock index 8bb287d266b7d6d9edfc83524da52a4a447d7eac..5a3ec46f8893aaa8bb2ce7c0c85aa974709f9861 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -772,7 +772,7 @@ GEM retriable (3.1.1) rinku (2.0.0) rotp (2.1.2) - rouge (3.2.0) + rouge (3.2.1) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 3e610a4088ccfe4f49346802a062bd3f630c2994..bfc8d9b03ad01f9baa0d0af8badf70ee58541538 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -203,7 +203,7 @@ export default { this.showIssueForm = !this.showIssueForm; }, onScroll() { - if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { + if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { this.loadNextPage(); } }, diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f9badb01535ca91c0f61ae353a0d5be0c9dab020..f55aa843444b7e41def9a0d89433016a52000890 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -133,7 +133,6 @@ export default { .then(() => this.getRawFileData({ path: this.file.path, - baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', }), ) .then(() => { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index c9795750d65f4b5926936dbd9d90760960b717f4..28b9d0df20142225f14834a40e3a00af9a2214d2 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -92,7 +92,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => { commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); }; -export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { +export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => { const file = state.entries[path]; return new Promise((resolve, reject) => { service @@ -100,6 +100,9 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = .then(raw => { if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (file.mrChange && file.mrChange.new_file === false) { + const baseSha = + (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || ''; + service .getBaseRawFileData(file, baseSha) .then(baseRaw => { @@ -122,7 +125,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = action: payload => dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)), actionText: __('Please try again'), - actionPayload: { path, baseSha }, + actionPayload: { path }, }); reject(); }); diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 1eda576870991de83508847f42613a653d64da51..56a8d9430c701b07e65074d2f0746326b88c59af 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -200,6 +200,7 @@ export default { }, [types.DELETE_ENTRY](state, path) { const entry = state.entries[path]; + const { tempFile = false } = entry; const parent = entry.parentPath ? state.entries[entry.parentPath] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; @@ -209,7 +210,11 @@ export default { parent.tree = parent.tree.filter(f => f.path !== entry.path); if (entry.type === 'blob') { - state.changedFiles = state.changedFiles.concat(entry); + if (tempFile) { + state.changedFiles = state.changedFiles.filter(f => f.path !== path); + } else { + state.changedFiles = state.changedFiles.concat(entry); + } } }, [types.RENAME_ENTRY](state, { path, name, entryPath = null }) { diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue new file mode 100644 index 0000000000000000000000000000000000000000..7f4852955139708bde7a26b83b6d5e7f1e5041ca --- /dev/null +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -0,0 +1,64 @@ +<script> +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + components: { + ClipboardButton, + }, + props: { + pipelineShortSha: { + type: String, + required: true, + }, + pipelineShaPath: { + type: String, + required: true, + }, + mergeRequestReference: { + type: String, + required: false, + default: null, + }, + mergeRequestPath: { + type: String, + required: false, + default: null, + }, + gitCommitTitlte: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="block"> + <p> + {{ __('Commit') }} + + <a + :href="pipelineShaPath" + class="js-commit-sha commit-sha link-commit" + > + {{ pipelineShortSha }} + </a> + + <clipboard-button + :text="pipelineShortSha" + :title="__('Copy commit SHA to clipboard')" + /> + + <a + v-if="mergeRequestPath && mergeRequestReference" + :href="mergeRequestPath" + class="js-link-commit link-commit" + > + {{ mergeRequestReference }} + </a> + </p> + + <p class="build-light-text append-bottom-0"> + {{ gitCommitTitlte }} + </p> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue new file mode 100644 index 0000000000000000000000000000000000000000..4faf08387fb4fa95c9fd0d67a035f9db917a2102 --- /dev/null +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -0,0 +1,76 @@ +<script> + export default { + props: { + illustrationPath: { + type: String, + required: true, + }, + illustrationSizeClass: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + content: { + type: String, + required: false, + default: null, + }, + action: { + type: Object, + required: false, + default: null, + validator(value) { + return ( + value === null || + (Object.prototype.hasOwnProperty.call(value, 'link') && + Object.prototype.hasOwnProperty.call(value, 'method') && + Object.prototype.hasOwnProperty.call(value, 'title')) + ); + }, + }, + }, + }; +</script> +<template> + <div class="row empty-state"> + <div class="col-12"> + <div + :class="illustrationSizeClass" + class="svg-content" + > + <img :src="illustrationPath" /> + </div> + </div> + + <div class="col-12"> + <div class="text-content"> + <h4 class="js-job-empty-state-title text-center"> + {{ title }} + </h4> + + <p + v-if="content" + class="js-job-empty-state-content" + > + {{ content }} + </p> + + <div + v-if="action" + class="text-center" + > + <a + :href="action.link" + :data-method="action.method" + class="js-job-empty-state-action btn btn-primary" + > + {{ action.title }} + </a> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue new file mode 100644 index 0000000000000000000000000000000000000000..513851e376f999de7c7aa188f82abdf0b5329a39 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -0,0 +1,139 @@ +<script> + import Icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import { numberToHumanSize } from '~/lib/utils/number_utils'; + import { s__, sprintf } from '~/locale'; + + export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + canEraseJob: { + type: Boolean, + required: true, + }, + size: { + type: Number, + required: true, + }, + rawTracePath: { + type: String, + required: false, + default: null, + }, + canScrollToTop: { + type: Boolean, + required: true, + }, + canScrollToBottom: { + type: Boolean, + required: true, + }, + }, + computed: { + jobLogSize() { + return sprintf('Showing last %{startSpanTag} %{size} %{endSpanTag} of log -', { + startSpanTag: '<span class="s-truncated-info-size truncated-info-size">', + endSpanTag: '</span>', + size: numberToHumanSize(this.size), + }); + }, + }, + methods: { + handleEraseJobClick() { + // eslint-disable-next-line no-alert + if (window.confirm(s__('Job|Are you sure you want to erase this job?'))) { + this.$emit('eraseJob'); + } + }, + handleScrollToTop() { + this.$emit('scrollJobLogTop'); + }, + handleScrollToBottom() { + this.$emit('scrollJobLogBottom'); + }, + }, + }; +</script> +<template> + <div class="top-bar"> + <!-- truncate information --> + <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> + <p v-html="jobLogSize"></p> + + <a + v-if="rawTracePath" + :href="rawTracePath" + class="js-raw-link raw-link" + > + {{ s__("Job|Complete Raw") }} + </a> + </div> + <!-- eo truncate information --> + + <div class="controllers float-right"> + <!-- links --> + <a + v-tooltip + v-if="rawTracePath" + :title="s__('Job|Show complete raw')" + :href="rawTracePath" + class="js-raw-link-controller controllers-buttons" + data-container="body" + > + <icon name="doc-text" /> + </a> + + <button + v-tooltip + v-if="canEraseJob" + :title="s__('Job|Erase job log')" + type="button" + class="js-erase-link controllers-buttons" + data-container="body" + @click="handleEraseJobClick" + > + <icon name="remove" /> + </button> + <!-- eo links --> + + <!-- scroll buttons --> + <div + v-tooltip + :title="s__('Job|Scroll to top')" + class="controllers-buttons" + data-container="body" + > + <button + :disabled="!canScrollToTop" + type="button" + class="js-scroll-top btn-scroll btn-transparent btn-blank" + @click="handleScrollToTop" + > + <icon name="scroll_up"/> + </button> + </div> + + <div + v-tooltip + :title="s__('Job|Scroll to bottom')" + class="controllers-buttons" + data-container="body" + > + <button + :disabled="!canScrollToBottom" + type="button" + class="js-scroll-bottom btn-scroll btn-transparent btn-blank" + @click="handleScrollToBottom" + > + <icon name="scroll_down"/> + </button> + </div> + <!-- eo scroll buttons --> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue new file mode 100644 index 0000000000000000000000000000000000000000..8a88e5da6aa6a2c9c3396342235389863c0f6a48 --- /dev/null +++ b/app/assets/javascripts/jobs/components/trigger_block.vue @@ -0,0 +1,84 @@ +<script> + export default { + props: { + shortToken: { + type: String, + required: false, + default: null, + }, + + variables: { + type: Object, + required: false, + default: () => ({}), + }, + }, + data() { + return { + areVariablesVisible: false, + }; + }, + computed: { + hasVariables() { + return Object.keys(this.variables).length > 0; + }, + }, + methods: { + revealVariables() { + this.areVariablesVisible = true; + }, + }, + }; +</script> + +<template> + <div class="build-widget block"> + <h4 class="title"> + {{ __('Trigger') }} + </h4> + + <p + v-if="shortToken" + class="js-short-token" + > + <span class="build-light-text"> + {{ __('Token') }} + </span> + {{ shortToken }} + </p> + + <p v-if="hasVariables"> + <button + type="button" + class="btn btn-default group js-reveal-variables" + @click="revealVariables" + > + {{ __('Reveal Variables') }} + </button> + + </p> + + <dl + v-if="areVariablesVisible" + class="js-build-variables trigger-build-variables" + > + <template + v-for="(value, key) in variables" + > + <dt + :key="`${key}-variable`" + class="js-build-variable trigger-build-variable" + > + {{ key }} + </dt> + + <dd + :key="`${key}-value`" + class="js-build-value trigger-build-value" + > + {{ value }} + </dd> + </template> + </dl> + </div> +</template> diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js similarity index 100% rename from app/assets/javascripts/pages/admin/cohorts/index.js rename to app/assets/javascripts/pages/instance_statistics/cohorts/index.js diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js similarity index 100% rename from app/assets/javascripts/pages/admin/cohorts/usage_ping.js rename to app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js similarity index 100% rename from app/assets/javascripts/pages/admin/conversational_development_index/show/index.js rename to app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index af91497d0eacf0d405fa0fe5fd45fbed7e7b7dea..eac1345742d3310ce4b4f11ee2d0125a3dc1cd0c 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1293,6 +1293,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; &.build-page .top-bar { top: 0; + height: auto; font-size: 12px; border-top-right-radius: $border-radius-default; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 8d28daac75003a35fc95e85c4d2b3f2540181d42..2e1b21268876a6ff0191717238d72b189972be97 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -141,9 +141,6 @@ ul.notes { } .note-body { - overflow-x: auto; - overflow-y: hidden; - .note-text { @include md-typography; // Reset ul style types since we're nested inside a ul already diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index fe5f68ba3d502dc97ca2f402bec786d352d90505..e032f568913f185047013e9e24ed2b46a837ed89 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbuseReportMailer < BaseMailer def notify(abuse_report_id) return unless deliverable? diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 654468bc7fe478d014dc9d63149a3b2dd026e895..5fd209c4761d2dd38506cc714d48628c2609365d 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BaseMailer < ActionMailer::Base around_action :render_with_default_locale diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 962570a0efd1ec211d4f4c3348fbb6823570c0b1..7aa75ee30e625ed16ecd6cbeab9d3f84a664df3e 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to @@ -9,8 +11,9 @@ class DeviseMailer < Devise::Mailer protected def subject_for(key) - subject = super - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [super] + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end end diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb index 76db31a4c4546e1f75c8739afd49f91e4b2b92c0..45fc5a6c3837ce3e6dddf3b2354842744f9c2b6c 100644 --- a/app/mailers/email_rejection_mailer.rb +++ b/app/mailers/email_rejection_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailer < BaseMailer def rejection(reason, original_raw, can_retry = false) @reason = reason diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 392cc0bee03dcf8fc69595977d55ef2ce83dabd0..c8b1ab5033a99df5f7dd8be52ddc35577b7a3e84 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Issues def new_issue_email(recipient_id, issue_id, reason = nil) diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 75cf56a51f239d51f53523cc9609ca88257a24f7..91dfdf589824a71981f0f22e91ece7674375e44e 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Members extend ActiveSupport::Concern diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index eaa0fbeffaa05ff359c7fa563497251a4447bf50..ced867f54979e6dc8784d0aa0aea9304bf8ad588 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module MergeRequests def new_merge_request_email(recipient_id, merge_request_id, reason = nil) diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 95a3bc8c56d08f603124948bc27601ac1af86069..a11b6b41e612fbdd694fc381819f3b599f2f3d2b 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Notes prepend Emails::EE::Notes diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb index 0027dfdc36b1d9bc4b440e15f13b19d0f22f47bc..ce449237ef642b97675b3c0add995fb369a78e26 100644 --- a/app/mailers/emails/pages_domains.rb +++ b/app/mailers/emails/pages_domains.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module PagesDomains def pages_domain_enabled_email(domain, recipient) diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index f9f45ab987b39c08429f59eed603ec752e6521d2..31e183640adc4022db38ae350fe341534d992fc8 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Pipelines def pipeline_success_email(pipeline, recipients) @@ -39,10 +41,10 @@ def add_pipeline_headers end def pipeline_subject(status) - commit = @pipeline.short_sha - commit << " in #{@merge_request.to_reference}" if @merge_request + commit = [@pipeline.short_sha] + commit << "in #{@merge_request.to_reference}" if @merge_request - subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit) + subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit.join(' ')) end end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 4f5edeb9bda172872def7fd8a6d7396ff77a5e62..40d7b9ccd7a83c01ae2733f1b4c5c72ca4182cfc 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Profile def new_user_email(user_id, token = nil) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 1a3913bc29940174034f057df69bca2c157f7ed0..f079694ccc125f5197769689e5f4d73821336f22 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Projects prepend Emails::EE::Projects diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 33da2d97ef4ce9fbcf85ca6cf5b2f97ddec8ee73..859c6c41dd34f85861f3d78506098afcf4410589 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Notify < BaseMailer prepend ::EE::Notify @@ -94,12 +96,14 @@ def recipient(recipient_id) # >> subject('Lorem ipsum', 'Dolor sit amet') # => "Lorem ipsum | Dolor sit amet" def subject(*extra) - subject = "" - subject << "#{@project.name} | " if @project - subject << "#{@group.name} | " if @group - subject << extra.join(' | ') if extra.present? - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [] + + subject << @project.name if @project + subject << @group.name if @group + subject.concat(extra) if extra.present? + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end # Return a string suitable for inclusion in the 'Message-Id' mail header. diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb index d6588efc4860ab2c9675da45f06b69eb29f08a68..3b9ef0d3ac04fb833af2f3aac5e79f7cdbcd04ad 100644 --- a/app/mailers/previews/devise_mailer_preview.rb +++ b/app/mailers/previews/devise_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailerPreview < ActionMailer::Preview def confirmation_instructions_for_signup DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {}) diff --git a/app/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb index 639e8471232f0704e5ff6fff25ab2b9ec3b00a3d..402066151ef074fc793aa2fa3f9af8fd0eac2dd1 100644 --- a/app/mailers/previews/email_rejection_mailer_preview.rb +++ b/app/mailers/previews/email_rejection_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailerPreview < ActionMailer::Preview def rejection EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index c55e40142410340282d9f380fb54030e32ab5faa..a29bf72d6e38f0ce15a9c7624243d01750e18747 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotifyPreview < ActionMailer::Preview prepend EE::Preview::NotifyPreview diff --git a/app/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb index 19d4eab18054dc3cddf62a2ac1d5fde7abbdd9b1..834d7594719545504102e7bb4b88ddd3b59795e8 100644 --- a/app/mailers/previews/repository_check_mailer_preview.rb +++ b/app/mailers/previews/repository_check_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailerPreview < ActionMailer::Preview def notify RepositoryCheckMailer.notify(3).message diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index 22a9f5da646fe3ec64c9929b1bae41f52c6121a2..4bcf371cfc039404978efe836f9d14ff19e6b5fd 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailer < BaseMailer def notify(failed_count) @message = diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6f7390c398872e26c93377fa93659fadbea2205a..ee825e78776055d4a840ce65f2d944144e10a422 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -71,6 +71,10 @@ def persisted_environment '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) end + scope :with_archived_trace, ->() do + where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) + end + scope :without_archived_trace, ->() do where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) end @@ -81,6 +85,7 @@ def persisted_environment end scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } + scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 7d34b52f2007a75a77202aa0d3cc7dc7c9718337..edfc1630ecc20765ad24ab78efad88f0082a6682 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,6 +11,7 @@ class Namespace < ActiveRecord::Base include Storage::LegacyNamespace include Gitlab::SQL::Pattern include IgnorableColumn + include FeatureGate ignore_column :deleted_at @@ -125,7 +126,6 @@ def visibility_level_field def to_param full_path end - alias_method :flipper_id, :to_param def human_name owner_name diff --git a/app/models/project.rb b/app/models/project.rb index bfc0a50646c3dc3a0b634d710e7aa70faf5325de..f8c336ae835879a8fb98fed98927a69c4c91dab8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -27,6 +27,7 @@ class Project < ActiveRecord::Base include FastDestroyAll::Helpers include WithUploads include BatchDestroyDependentAssociations + include FeatureGate extend Gitlab::Cache::RequestCache # EE specific modules @@ -534,18 +535,19 @@ def lfs_enabled? def auto_devops_enabled? if auto_devops&.enabled.nil? - Gitlab::CurrentSettings.auto_devops_enabled? + has_auto_devops_implicitly_enabled? else auto_devops.enabled? end end def has_auto_devops_implicitly_enabled? - auto_devops&.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled? + auto_devops&.enabled.nil? && + (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) end def has_auto_devops_implicitly_disabled? - auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled? + auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) end def empty_repo? diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 155400d1a43d0047c9c90e6376ed11fc08c17d07..dc6736dd9cdc97fde32a8e5a667f8b394dabfa24 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -47,12 +47,8 @@ def create_gitlab_deploy_token end def needs_to_create_deploy_token? - auto_devops_enabled? && + project.auto_devops_enabled? && !project.public? && !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present? end - - def auto_devops_enabled? - Gitlab::CurrentSettings.auto_devops_enabled? || enabled? - end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 228c1abae76fccdb72fe62d06abfe13fe4b33f8e..a836d9c7e33ed4d773d9f2a3fa02a847a0335288 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -32,7 +32,7 @@ def execute def run_auto_devops_pipeline? return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled') - project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?) + project.auto_devops_enabled? end private diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 4507218d023eee2942ea9bed58c7992986546dd0..274a1fcb5c3c2e59a0a95ef9a2074d8208a6c212 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -41,6 +41,8 @@ .form-text.text-muted Set the default expiration time for each job's artifacts. 0 for unlimited. + The default unit is in seconds, but you can define an alternative. For example: + <code>4 mins 2 sec</code>, <code>2h42min</code>. = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 249ada761dc3b016dce525c128de86b3b1b89caf..3e6ec90d9d87adfd02914f32b5fcbd592c3e2f2c 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -1,15 +1,18 @@ %fieldset %legend Access .form-group.row - = f.label :projects_limit, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :projects_limit, class: 'col-form-label' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' .form-group.row - = f.label :can_create_group, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :can_create_group, class: 'col-form-label' .col-sm-10= f.check_box :can_create_group .form-group.row - = f.label :access_level, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :access_level, class: 'col-form-label' .col-sm-10 - editing_current_user = (current_user == @user) @@ -36,7 +39,8 @@ You cannot remove your own admin rights. .form-group.row - = f.label :external, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :external, class: 'col-form-label' .col-sm-10 = f.check_box :external do External diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 9222e414f8c6e034aed65c26bc2b5eca54c63c42..ca1d6c1863d6a7156ff430cb5aa0a2c21c9e3f70 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -5,17 +5,20 @@ %fieldset %legend Account .form-group.row - = f.label :name, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :name, class: 'col-form-label' .col-sm-10 = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required .form-group.row - = f.label :username, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :username, class: 'col-form-label' .col-sm-10 = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required .form-group.row - = f.label :email, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :email, class: 'col-form-label' .col-sm-10 = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required @@ -24,7 +27,8 @@ %fieldset %legend Password .form-group.row - = f.label :password, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password, class: 'col-form-label' .col-sm-10 %strong Reset link will be generated and sent to the user. @@ -34,10 +38,12 @@ %fieldset %legend Password .form-group.row - = f.label :password, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password, class: 'col-form-label' .col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control' .form-group.row - = f.label :password_confirmation, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password_confirmation, class: 'col-form-label' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' = render partial: 'access_levels', locals: { f: f } @@ -53,21 +59,26 @@ %fieldset %legend Profile .form-group.row - = f.label :avatar, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :avatar, class: 'col-form-label' .col-sm-10 = f.file_field :avatar .form-group.row - = f.label :skype, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :skype, class: 'col-form-label' .col-sm-10= f.text_field :skype, class: 'form-control' .form-group.row - = f.label :linkedin, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :linkedin, class: 'col-form-label' .col-sm-10= f.text_field :linkedin, class: 'form-control' .form-group.row - = f.label :twitter, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :twitter, class: 'col-form-label' .col-sm-10= f.text_field :twitter, class: 'form-control' .form-group.row - = f.label :website_url, 'Website', class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :website_url, 'Website', class: 'col-form-label' .col-sm-10= f.text_field :website_url, class: 'form-control' %fieldset diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 434aed2f603259e4e46de98cc5aec5b41b31b452..9134257b6317f2f500780131197694aa12ea5171 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -17,7 +17,7 @@ %h5.prepend-top-0 = _("Git strategy for pipelines") %p - = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code") + = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .form-check = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } @@ -47,7 +47,7 @@ = f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold' = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' %p.form-text.text-muted - = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>") + = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' %hr diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 8a5abb64515941869d9dab6156f35bed4fd7077e..df8a574245015312c25743485377c9dae7dda481 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -21,8 +21,7 @@ %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) - - if Feature.enabled?(:repository_languages, @project.namespace.becomes(Namespace)) - = repository_languages_bar(@project.repository_languages) + = repository_languages_bar(@project.repository_languages) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - if @project.archived? diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb index cfbbbffc8a6ca3fe24d355f4fe6f52dc9aec190c..854b74b884a46616d2c3044efc8a6bfe0253f952 100644 --- a/app/workers/detect_repository_languages_worker.rb +++ b/app/workers/detect_repository_languages_worker.rb @@ -16,8 +16,6 @@ def perform(project_id, user_id) user = User.find_by(id: user_id) return unless project && user - return if Feature.disabled?(:repository_languages, project.namespace) - try_obtain_lease do ::Projects::DetectRepositoryLanguagesService.new(project, user).execute end diff --git a/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml b/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml new file mode 100644 index 0000000000000000000000000000000000000000..bc0150c6586e5b25659d2d03c6854cdde3179c22 --- /dev/null +++ b/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml @@ -0,0 +1,5 @@ +--- +title: "Fix If-Check the result that a function was executed several times" +merge_request: 20640 +author: Max Dicker +type: fixed diff --git a/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml b/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml new file mode 100644 index 0000000000000000000000000000000000000000..00e1f6e638a3bc2c2d1bb5709490426ca2d93637 --- /dev/null +++ b/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml @@ -0,0 +1,5 @@ +--- +title: Fixes input alignment in user admin form with errors +merge_request: 21108 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/50101-commit-block.yml b/changelogs/unreleased/50101-commit-block.yml new file mode 100644 index 0000000000000000000000000000000000000000..f6bad4c8154e3e13a322dcc3d99b63ace606f3ff --- /dev/null +++ b/changelogs/unreleased/50101-commit-block.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for commit block in job log page +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-empty-state-component.yml b/changelogs/unreleased/50101-empty-state-component.yml new file mode 100644 index 0000000000000000000000000000000000000000..ee99b65d9642ec6e8e69c17d05031f345cdbef13 --- /dev/null +++ b/changelogs/unreleased/50101-empty-state-component.yml @@ -0,0 +1,5 @@ +--- +title: Creates empty state vue component for job view +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-trigger.yml b/changelogs/unreleased/50101-trigger.yml new file mode 100644 index 0000000000000000000000000000000000000000..df4243afa636fb347108f3e9486975f7e32acfa8 --- /dev/null +++ b/changelogs/unreleased/50101-trigger.yml @@ -0,0 +1,5 @@ +--- +title: Creates Vue component for trigger variables block in job log page +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-truncated-job-information.yml b/changelogs/unreleased/50101-truncated-job-information.yml new file mode 100644 index 0000000000000000000000000000000000000000..b873b8b7bf694d4e0f498a6d44310aaa56034b37 --- /dev/null +++ b/changelogs/unreleased/50101-truncated-job-information.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for job log top bar with controllers +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml b/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml new file mode 100644 index 0000000000000000000000000000000000000000..50a3b9c9affdc1abf4699a9a58d572b79271a061 --- /dev/null +++ b/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue stopping Instance Statistics javascript to be executed +merge_request: 21211 +author: +type: fixed diff --git a/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml b/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml new file mode 100644 index 0000000000000000000000000000000000000000..d963dc5bac34c78be651d176910cee47531b3819 --- /dev/null +++ b/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml @@ -0,0 +1,5 @@ +--- +title: Add an example of the configuration of archive trace cron worker in gitlab.yml.example +merge_request: 20583 +author: +type: other diff --git a/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml b/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml new file mode 100644 index 0000000000000000000000000000000000000000..b82344e3c9c7f990aa460f4332189b8a7476f6cb --- /dev/null +++ b/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml @@ -0,0 +1,5 @@ +--- +title: Add rake command to migrate archived traces from local storage to object storage +merge_request: 21193 +author: +type: added diff --git a/changelogs/unreleased/emoji-cutoff-1px.yml b/changelogs/unreleased/emoji-cutoff-1px.yml new file mode 100644 index 0000000000000000000000000000000000000000..815d9c177e843c4ff035231696c7a009ff9bbfa4 --- /dev/null +++ b/changelogs/unreleased/emoji-cutoff-1px.yml @@ -0,0 +1,5 @@ +--- +title: Fix 1px cutoff of emojis +merge_request: 21180 +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/frozen-string-enable-app-mailers.yml b/changelogs/unreleased/frozen-string-enable-app-mailers.yml new file mode 100644 index 0000000000000000000000000000000000000000..2cd247ca76cdd251384870a00e2d0e5225032d13 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-app-mailers.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen in app/mailers/**/*.rb +merge_request: 21147 +author: gfyoung +type: performance diff --git a/changelogs/unreleased/ide-delete-new-files-state.yml b/changelogs/unreleased/ide-delete-new-files-state.yml new file mode 100644 index 0000000000000000000000000000000000000000..500115d19d08d8b2b29c9afa2e30596d83fb7af4 --- /dev/null +++ b/changelogs/unreleased/ide-delete-new-files-state.yml @@ -0,0 +1,5 @@ +--- +title: Fixed IDE deleting new files creating wrong state +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/ide-job-top-bar-ui-polish.yml b/changelogs/unreleased/ide-job-top-bar-ui-polish.yml new file mode 100644 index 0000000000000000000000000000000000000000..d917c14e5f8b079aed4fdce73d9e3d4339289e81 --- /dev/null +++ b/changelogs/unreleased/ide-job-top-bar-ui-polish.yml @@ -0,0 +1,5 @@ +--- +title: Improved styling of top bar in IDE job trace pane +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml new file mode 100644 index 0000000000000000000000000000000000000000..bcf3d2c8e16bd6949f3080b9184ce850ec5dc023 --- /dev/null +++ b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml @@ -0,0 +1,6 @@ +--- +title: Combines emoji award spec files into single user_interacts_with_awards_in_issue_spec.rb + file +merge_request: 21126 +author: Nate Geslin +type: other diff --git a/changelogs/unreleased/rails5-verbose-query-logs.yml b/changelogs/unreleased/rails5-verbose-query-logs.yml new file mode 100644 index 0000000000000000000000000000000000000000..7585e75d30b0f6a3982e995145c719ca9eaca646 --- /dev/null +++ b/changelogs/unreleased/rails5-verbose-query-logs.yml @@ -0,0 +1,5 @@ +--- +title: 'Rails5: Enable verbose query logs' +merge_request: 21231 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/rouge_3-2-1.yml b/changelogs/unreleased/rouge_3-2-1.yml new file mode 100644 index 0000000000000000000000000000000000000000..b281a4f0e95bf8692753d64138707cbc2219052b --- /dev/null +++ b/changelogs/unreleased/rouge_3-2-1.yml @@ -0,0 +1,5 @@ +--- +title: Update to Rouge 3.2.1, which includes a critical fix to the Perl Lexer +merge_request: 21263 +author: +type: changed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index cc0a07600bc5de3c4530a25135c6157b14df283a..e05954d9259590807b92eead1b7c30ca8f54b710 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -281,6 +281,9 @@ production: &base # once per hour you will have concurrent 'git fsck' jobs. repository_check_worker: cron: "20 * * * *" + # Archive live traces which have not been archived yet + ci_archive_traces_cron_worker: + cron: "17 * * * *" # Send admin emails once a week admin_email_worker: cron: "0 0 * * 0" diff --git a/config/initializers/active_record_verbose_query_logs.rb b/config/initializers/active_record_verbose_query_logs.rb index 44f86fec7e09389320ddb7eae04643a60ff51ac1..1c5fbc8e830240986e5121622144ffd1a2b3f438 100644 --- a/config/initializers/active_record_verbose_query_logs.rb +++ b/config/initializers/active_record_verbose_query_logs.rb @@ -47,7 +47,9 @@ def ignored_callstack(path) end end - unless Gitlab.rails5? + if Rails.version.start_with?("5.2") + raise "Remove this monkey patch: #{__FILE__}" + else prepend(VerboseQueryLogs) unless Rails.env.production? end end diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md index 24d1a3fd1518b926dbbd0a58288d6d2bb7adcfc0..6e2f67f61bc4c5f8cd26c06e5af335d85aa373cc 100644 --- a/doc/administration/job_traces.md +++ b/doc/administration/job_traces.md @@ -3,10 +3,6 @@ Job traces are sent by GitLab Runner while it's processing a job. You can see traces in job pages, pipelines, email notifications, etc. -There isn't a way to automatically expire old job logs, but it's safe to remove -them if they're taking up too much space. If you remove the logs manually, the -job output in the UI will be empty. - ## Data flow In general, there are two states in job traces: "live trace" and "archived trace". @@ -57,11 +53,55 @@ To change the location where the job logs will be stored, follow the steps below ## Uploading traces to object storage -An archived trace is considered as a [job artifact](job_artifacts.md). -Therefore, when you [set up an object storage](job_artifacts.md#object-storage-settings), +Archived traces are considered as [job artifacts](job_artifacts.md). +Therefore, when you [set up the object storage integration](job_artifacts.md#object-storage-settings), job traces are automatically migrated to it along with the other job artifacts. -See [Data flow](#data-flow) to learn about the process. +See "Phase 4: uploading" in [Data flow](#data-flow) to learn about the process. + +## How to archive legacy job trace files + +Legacy job traces, which were created before GitLab 10.5, were not archived regularly. +It's the same state with the "2: overwriting" in the above [Data flow](#data-flow). +To archive those legacy job traces, please follow the instruction below. + +1. Execute the following command + + ```bash + gitlab-rake gitlab:traces:archive + ``` + + After you executed this task, GitLab instance queues up Sidekiq jobs (asynchronous processes) + for migrating job trace files from local storage to object storage. + It could take time to complete the all migration jobs. You can check the progress by the following command + + ```bash + sudo gitlab-rails console + ``` + + ```bash + [1] pry(main)> Sidekiq::Stats.new.queues['pipeline_background:archive_trace'] + => 100 + ``` + + If the count becomes zero, the archiving processes are done + +## How to migrate archived job traces to object storage + +If job traces have already been archived into local storage, and you want to migrate those traces to object storage, please follow the instruction below. + +1. Ensure [Object storage integration for Job Artifacts](job_artifacts.md#object-storage-settings) is enabled +1. Execute the following command + + ```bash + gitlab-rake gitlab:traces:migrate + ``` + +## How to remove job traces + +There isn't a way to automatically expire old job logs, but it's safe to remove +them if they're taking up too much space. If you remove the logs manually, the +job output in the UI will be empty. ## New live trace architecture diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md index b0e01d74f7ea9afcda98d826da5b0f2e8d52a8c6..8ae80b2bc02fb4561da5ffaf65ca1963aaaf8a00 100644 --- a/doc/ci/docker/README.md +++ b/doc/ci/docker/README.md @@ -6,3 +6,4 @@ comments: false - [Using Docker Images](using_docker_images.md) - [Using Docker Build](using_docker_build.md) +- [Using kaniko](using_kaniko.md) diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md new file mode 100644 index 0000000000000000000000000000000000000000..7d4f28e1f47ea5eef2af590f22f6d4c3b8aeb1d0 --- /dev/null +++ b/doc/ci/docker/using_kaniko.md @@ -0,0 +1,60 @@ +# Building images with kaniko and GitLab CI/CD + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45512) in GitLab 11.2. +Requires GitLab Runner 11.2 and above. + +[kaniko](https://github.com/GoogleContainerTools/kaniko) is a tool to build +container images from a Dockerfile, inside a container or Kubernetes cluster. + +kaniko solves two problems with using the +[docker-in-docker build](using_docker_build.md#use-docker-in-docker-executor) method: + +1. Docker-in-docker requires [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities) + in order to function, which is a significant security concern. +1. Docker-in-docker generally incurs a performance penalty and can be quite slow. + +## Requirements + +In order to utilize kaniko with GitLab, a [GitLab Runner](https://docs.gitlab.com/runner/) +using either the [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html), +[Docker](https://docs.gitlab.com/runner/executors/docker.html), or +[Docker Machine](https://docs.gitlab.com/runner/executors/docker_machine.html) +executors is required. + +## Building a Docker image with kaniko + +When building an image with kaniko and GitLab CI/CD, you should be aware of a +few important details: + +- The kaniko debug image is recommended (`gcr.io/kaniko-project/executor:debug`) + because it has a shell, and a shell is required for an image to be used with + GitLab CI/CD. +- The entrypoint will need to be [overridden](using_docker_images.md#overriding-the-entrypoint-of-an-image), + otherwise the build script will not run. +- A Docker `config.json` file needs to be created with the authentication + information for the desired container registry. + +--- + +In the following example, kaniko is used to build a Docker image and then push +it to [GitLab Container Registry](../../user/project/container_registry.md). +The job will run only when a tag is pushed. A `config.json` file is created under +`/root/.docker` with the needed GitLab Container Registry credentials taken from the +[environment variables](../variables/README.md#predefined-variables-environment-variables) +GitLab CI/CD provides. In the last step, kaniko uses the `Dockerfile` under the +root directory of the project, builds the Docker image and pushes it to the +project's Container Registry while tagging it with the Git tag: + +```yaml +build: + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + script: + - mkdir -p /root/.docker + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /root/.docker/config.json + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG + only: + - tags +``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index b4854e5dd949a588dbfe67ec1e00a7d14dfe6a78..d9b2a4fc9d227f98d4110327bc1511805673fe9e 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1075,8 +1075,10 @@ keep artifacts forever. After their expiry, artifacts are deleted hourly by default (via a cron job), and are not accessible anymore. -The value of `expire_in` is an elapsed time. Examples of parsable values: +The value of `expire_in` is an elapsed time in seconds, unless a unit is +provided. Examples of parsable values: +- '42' - '3 mins 4 sec' - '2 hrs 20 min' - '2h20min' diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index 5d1f657015c9e971e3cf6e36ff08e2b33e078d03..09ea8c05be6708e26217bbb988c05ab99af12699 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -20,7 +20,40 @@ dynamic (querying the DB etc.). Once defined in `lib/feature.rb`, you will be able to activate a feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature) +For GitLab.com, team members have access to feature flags through chatops. Only +percentage gates are supported at this time. Setting a feature to be used 50% of +the time, you should execute `/chatops run feature set my_feature_flag 50`. + ## Feature flags for user applications GitLab does not yet support the use of feature flags in deployed user applications. -You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779). \ No newline at end of file +You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779). + +## Developing with feature flags + +In general, it's better to have a group- or user-based gate, and you should prefer +it over the use of percentage gates. This would make debugging easier, as you +filter for example logs and errors based on actors too. Futhermore, this allows +for enabling for the `gitlab-org` group first, while the rest of the users +aren't impacted. + +```ruby +# Good +Feature.enabled?(:feature_flag, project) + +# Avoid, if possible +Feature.enabled?(:feature_flag) +``` + +To use feature gates based on actors, the model needs to respond to +`flipper_id`. For example, to enable for the Foo model: + +```ruby +class Foo < ActiveRecord::Base + include FeatureGate +end +``` + +Features that are developed and are intended to be merged behind a feature flag +should not include a changelog entry. The entry should be added in the merge +request removing the feature flags. diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 3467234a5fbee3602e03968ff4cf8d6700c9d338..7e52ada50730d8c980fafe97d9cdcf0b0a5d22cf 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -233,6 +233,11 @@ in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that all the projects that haven't explicitly set an option will have Auto DevOps enabled by default. +NOTE: **Note:** +There is also a feature flag to enable Auto DevOps to a percentage of projects +which can be enabled from the console with +`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`. + ### Deployment strategy > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 6fdcb244c9074ec3315e96b592778ca8518b403d..83db8ce585c365ea8a89d81a146d166ae957a9c2 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -21,8 +21,9 @@ that this setting is set for each job. The default expiration time of the [job artifacts][art-yml] can be set in the Admin area of your GitLab instance. The syntax of duration is described in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that -this setting is set for each job. Set it to 0 if you don't want default -expiration. +this setting is set for each job. Set it to `0` if you don't want default +expiration. The default unit is in seconds. + 1. Go to **Admin area > Settings** (`/admin/application_settings`). diff --git a/doc/user/markdown.md b/doc/user/markdown.md index f84a90b2c0c13352d20599a11c48fad2ebc37ac7..7a20eaa4b396895af454ff6f63ba310851691494 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -235,7 +235,12 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: - Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. + Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. + + On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. + + Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. + Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: @@ -247,7 +252,13 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: -Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. +Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. + +On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. + +Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. + + ### Special GitLab References diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md index 296e30aa0c327e5c3f6b3e7dbe02594f80074e2e..24bf6541a9db126210f114baaccf24fc32cb38eb 100644 --- a/doc/user/project/import/manifest.md +++ b/doc/user/project/import/manifest.md @@ -1,5 +1,8 @@ # Import multiple repositories by uploading a manifest file +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28811) in +GitLab 11.2. + GitLab allows you to import all the required Git repositories based on a manifest file like the one used by the [Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml). diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index 6ab44420a10472c4305ff8f5cd1e4329fed65a64..47525617d9575861969994c69dc39ed08ec5a59a 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -1,5 +1,7 @@ # Hangouts Chat service +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/43756) in GitLab 11.2. + The Hangouts Chat service sends notifications from GitLab to the room for which the webhook was created. ## On Hangouts Chat diff --git a/doc/user/project/repository/img/repository_languages.png b/doc/user/project/repository/img/repository_languages.png new file mode 100644 index 0000000000000000000000000000000000000000..d9fb1278e06c3658e7c7c5b1b48d74c9e7f64556 Binary files /dev/null and b/doc/user/project/repository/img/repository_languages.png differ diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index f39ab5ad92dc5826d607b4c4fc42da54e3c72b1d..150cc4b4f69c5450c913d74a41828e2b0907631a 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -155,6 +155,16 @@ The repository graph displays visually the Git flow strategy used in that reposi Find it under your project's **Repository > Graph**. +## Repository Languages + +For the default branch of each repository, GitLab will determine what programming languages +were used and display this on the projects pages. + + + +Not all files are detected, among others; documentation, +vendored code, and most markup languages are excluded. + ## Compare Select branches to compare and view the changes inline: diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index e9c901f8592dcaa00a0245a54d6e427f68c11585..9521a2d63a0ae784152fff3b7fbc7f3c17561817 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -543,14 +543,8 @@ def add_tag(tag_name, user:, target:, message: nil) end def update_branch(branch_name, user:, newrev:, oldrev:) - gitaly_migrate(:operation_user_update_branch) do |is_enabled| - if is_enabled - gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev) - else - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - OperationService.new(user, self).update_branch(branch_name, newrev, oldrev) - end - end + wrapped_gitaly_errors do + gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev) end end diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 65eb5cc18cf310ac20d418e63ab7e0f43c0c2cd4..752a91fbb602809ea4e6948b7911ff5e0eb1e432 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -2,34 +2,7 @@ module Gitlab module Git module RepositoryMirroring def remote_branches(remote_name) - gitaly_migrate(:ref_find_all_remote_branches) do |is_enabled| - if is_enabled - gitaly_ref_client.remote_branches(remote_name) - else - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - rugged_remote_branches(remote_name) - end - end - end - end - - private - - def rugged_remote_branches(remote_name) - branches = [] - - rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref| - name = ref.name.sub(%r{\Arefs/remotes/#{remote_name}/}, '') - - begin - target_commit = Gitlab::Git::Commit.find(self, ref.target.oid) - branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit) - rescue Rugged::ReferenceError - # Omit invalid branch - end - end - - branches + gitaly_ref_client.remote_branches(remote_name) end end end diff --git a/lib/tasks/gitlab/traces.rake b/lib/tasks/gitlab/traces.rake index ddcca69711f80714c0bae1a5b2acd50dc4283eae..5a232091a7edd8febf93d2a8f3b636562820a0f8 100644 --- a/lib/tasks/gitlab/traces.rake +++ b/lib/tasks/gitlab/traces.rake @@ -18,5 +18,22 @@ namespace :gitlab do logger.info("Scheduled #{job_ids.count} jobs. From #{job_ids.min} to #{job_ids.max}") end end + + task migrate: :environment do + logger = Logger.new(STDOUT) + logger.info('Starting transfer of job traces') + + Ci::Build.joins(:project) + .with_archived_trace_stored_locally + .find_each(batch_size: 10) do |build| + begin + build.job_artifacts_trace.file.migrate!(ObjectStorage::Store::REMOTE) + + logger.info("Transferred job trace of #{build.id} to object storage") + rescue => e + logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}") + end + end + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index eecf3a9a2771fddd30f661dc0aaace22178aa212..81cdc999facea423e211b98529def8ad6806b61c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4073,12 +4073,21 @@ msgstr "" msgid "Jobs" msgstr "" +msgid "Job|Are you sure you want to erase this job?" +msgstr "" + msgid "Job|Browse" msgstr "" +msgid "Job|Complete Raw" +msgstr "" + msgid "Job|Download" msgstr "" +msgid "Job|Erase job log" +msgstr "" + msgid "Job|Job artifacts" msgstr "" @@ -4091,6 +4100,15 @@ msgstr "" msgid "Job|Keep" msgstr "" +msgid "Job|Scroll to bottom" +msgstr "" + +msgid "Job|Scroll to top" +msgstr "" + +msgid "Job|Show complete raw" +msgstr "" + msgid "Job|The artifacts were removed" msgstr "" @@ -6069,6 +6087,9 @@ msgstr "" msgid "Retry verification" msgstr "" +msgid "Reveal Variables" +msgstr "" + msgid "Reveal value" msgid_plural "Reveal values" msgstr[0] "" @@ -7464,6 +7485,9 @@ msgstr "" msgid "Trending" msgstr "" +msgid "Trigger" +msgstr "" + msgid "Trigger pipelines for mirror updates" msgstr "" diff --git a/spec/features/instance_statistics/cohorts_spec.rb b/spec/features/instance_statistics/cohorts_spec.rb index 81fc5eff980c0323f6d4ef01b33fc0aad58eb79b..573f8600be18ffcf49f86d707534b7c3d60a5055 100644 --- a/spec/features/instance_statistics/cohorts_spec.rb +++ b/spec/features/instance_statistics/cohorts_spec.rb @@ -12,4 +12,12 @@ expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") end + + it 'shows usage data', :js do + visit instance_statistics_cohorts_path + + wait_for_requests + + expect(find('.js-syntax-highlight').text).not_to eq('') + end end diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb index d441a7a5af9bc833dc94141d7ec3a4acb09a1a02..a6c16b6a2a3421d6403e35576ba0a07a7d7add10 100644 --- a/spec/features/instance_statistics/conversational_development_index_spec.rb +++ b/spec/features/instance_statistics/conversational_development_index_spec.rb @@ -5,6 +5,16 @@ sign_in(create(:admin)) end + it 'has dismissable intro callout', :js do + visit instance_statistics_conversational_development_index_index_path + + expect(page).to have_content 'Introducing Your Conversational Development Index' + + find('.js-close-callout').click + + expect(page).not_to have_content 'Introducing Your Conversational Development Index' + end + context 'when usage ping is disabled' do it 'shows empty state' do stub_application_setting(usage_ping_enabled: false) diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb deleted file mode 100644 index bf60b18873c81abc8cbc48ffd8d064d4d6bac470..0000000000000000000000000000000000000000 --- a/spec/features/issues/award_emoji_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'rails_helper' - -describe 'Awards Emoji' do - let!(:project) { create(:project, :public) } - let!(:user) { create(:user) } - let(:issue) do - create(:issue, - assignees: [user], - project: project) - end - - context 'authorized user' do - before do - project.add_maintainer(user) - sign_in(user) - end - - describe 'visiting an issue with a legacy award emoji that is not valid anymore' do - before do - # The `heart_tip` emoji is not valid anymore so we need to skip validation - issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) - visit project_issue_path(project, issue) - wait_for_requests - end - - # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 - it 'does not shows a 500 page', :js do - expect(page).to have_text(issue.title) - end - end - - describe 'Click award emoji from issue#show' do - let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } - - before do - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'increments the thumbsdown emoji', :js do - find('[data-name="thumbsdown"]').click - wait_for_requests - expect(thumbsdown_emoji).to have_text("1") - end - - context 'click the thumbsup emoji' do - it 'increments the thumbsup emoji', :js do - find('[data-name="thumbsup"]').click - wait_for_requests - expect(thumbsup_emoji).to have_text("1") - end - - it 'decrements the thumbsdown emoji', :js do - expect(thumbsdown_emoji).to have_text("0") - end - end - - context 'click the thumbsdown emoji' do - it 'increments the thumbsdown emoji', :js do - find('[data-name="thumbsdown"]').click - wait_for_requests - expect(thumbsdown_emoji).to have_text("1") - end - - it 'decrements the thumbsup emoji', :js do - expect(thumbsup_emoji).to have_text("0") - end - end - - it 'toggles the smiley emoji on a note', :js do - toggle_smiley_emoji(true) - - within('.note-body') do - expect(find(emoji_counter)).to have_text("1") - end - - toggle_smiley_emoji(false) - - within('.note-body') do - expect(page).not_to have_selector(emoji_counter) - end - end - - context 'execute /award quick action' do - it 'toggles the emoji award on noteable', :js do - execute_quick_action('/award :100:') - - expect(find(noteable_award_counter)).to have_text("1") - - execute_quick_action('/award :100:') - - expect(page).not_to have_selector(noteable_award_counter) - end - end - end - end - - context 'unauthorized user', :js do - before do - visit project_issue_path(project, issue) - end - - it 'has disabled emoji button' do - expect(first('.award-control')[:class]).to have_text('disabled') - end - end - - def execute_quick_action(cmd) - within('.js-main-target-form') do - fill_in 'note[note]', with: cmd - click_button 'Comment' - end - - wait_for_requests - end - - def thumbsup_emoji - page.all(emoji_counter).first - end - - def thumbsdown_emoji - page.all(emoji_counter).last - end - - def emoji_counter - 'span.js-counter' - end - - def noteable_award_counter - ".awards .active" - end - - def toggle_smiley_emoji(status) - within('.note') do - find('.note-emoji-button').click - end - - unless status - first('[data-name="smiley"]').click - else - find('[data-name="smiley"]').click - end - - wait_for_requests - end -end diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb deleted file mode 100644 index e53a4ce49c7c8587fb834d7981d6800a088fd272..0000000000000000000000000000000000000000 --- a/spec/features/issues/award_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -describe 'Issue awards', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project) } - - describe 'logged in' do - before do - sign_in(user) - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'adds award to issue' do - first('.js-emoji-btn').click - expect(page).to have_selector('.js-emoji-btn.active') - expect(first('.js-emoji-btn')).to have_content '1' - - visit project_issue_path(project, issue) - expect(first('.js-emoji-btn')).to have_content '1' - end - - it 'removes award from issue' do - first('.js-emoji-btn').click - find('.js-emoji-btn.active').click - expect(first('.js-emoji-btn')).to have_content '0' - - visit project_issue_path(project, issue) - expect(first('.js-emoji-btn')).to have_content '0' - end - - it 'only has one menu on the page' do - first('.js-add-award').click - expect(page).to have_selector('.emoji-menu') - - expect(page).to have_selector('.emoji-menu', count: 1) - end - end - - describe 'logged out' do - before do - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'does not see award menu button' do - expect(page).not_to have_selector('.js-award-holder') - end - end -end diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..afa425c2cec2ee76f85c736b4ecd036ffbab41ed --- /dev/null +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -0,0 +1,347 @@ +require 'spec_helper' + +describe 'User interacts with awards' do + let(:user) { create(:user) } + + describe 'User interacts with awards in an issue', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + + before do + project.add_maintainer(user) + sign_in(user) + + visit(project_issue_path(project, issue)) + end + + it 'toggles the thumbsup award emoji' do + page.within('.awards') do + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.award-control.js-emoji-btn') + expect(page.all('.award-control.js-emoji-btn').size).to eq(2) + + page.all('.award-control.js-emoji-btn').each do |element| + expect(element['title']).to eq('') + end + + expect(page.all('.award-control .js-counter')).to all(have_content('0')) + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + end + end + + it 'toggles a custom award emoji' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + page.within('.emoji-menu-content') do + emoji_button = page.first('.js-emoji-btn') + emoji_button.hover + emoji_button.click + end + + page.within('.awards') do + expect(page).to have_selector('.js-emoji-btn') + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + + expect do + page.find('.js-emoji-btn.active').click + wait_for_requests + end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) + end + end + + it 'shows the list of award emoji categories' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + fill_in('emoji-menu-search', with: 'hand') + + page.within('.emoji-menu-content') do + expect(page).to have_selector('[data-name="raised_hand"]') + end + end + + it 'adds an award emoji by a comment' do + page.within('.js-main-target-form') do + fill_in('note[note]', with: ':smile:') + + click_button('Comment') + end + + expect(page).to have_emoji('smile') + end + + context 'when a project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the add award button' do + page.within('.awards') do + expect(page).not_to have_css('.js-add-award') + end + end + end + + context 'User interacts with awards on a note' do + let!(:note) { create(:note, noteable: issue, project: issue.project) } + let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') } + + it 'shows the award on the note' do + page.within('.note-awards') do + expect(page).to have_emoji('100') + end + end + + it 'allows adding a vote to an award' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(2) + end + + it 'allows adding a new emoji' do + page.within('.note-actions') do + find('a.js-add-award').click + end + page.within('.emoji-menu-content') do + find('gl-emoji[data-name="8ball"]').click + end + wait_for_requests + + page.within('.note-awards') do + expect(page).to have_emoji('8ball') + end + expect(note.reload.award_emoji.size).to eq(2) + end + + context 'when the project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the buttons for adding new emoji' do + page.within('.note-awards') do + expect(page).not_to have_css('.award-menu-holder') + end + + page.within('.note-actions') do + expect(page).not_to have_css('a.js-add-award') + end + end + + it 'does not allow toggling existing emoji' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(1) + end + end + end + end + + describe 'User interacts with awards on an issue', :js do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + + describe 'logged in' do + before do + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'adds award to issue' do + first('.js-emoji-btn').click + + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + + visit project_issue_path(project, issue) + + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'removes award from issue' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + + expect(first('.js-emoji-btn')).to have_content '0' + + visit project_issue_path(project, issue) + + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'only has one menu on the page' do + first('.js-add-award').click + + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + describe 'logged out' do + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'does not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end + end + + describe 'Awards Emoji' do + let!(:project) { create(:project, :public) } + let(:issue) { create(:issue, assignees: [user], project: project) } + + context 'authorized user' do + before do + project.add_maintainer(user) + sign_in(user) + end + + describe 'visiting an issue with a legacy award emoji that is not valid anymore' do + before do + # The `heart_tip` emoji is not valid anymore so we need to skip validation + issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) + visit project_issue_path(project, issue) + wait_for_requests + end + + # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 + it 'does not shows a 500 page', :js do + expect(page).to have_text(issue.title) + end + end + + describe 'Click award emoji from issue#show' do + let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } + + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + context 'click the thumbsdown emoji' do + it 'increments the thumbsdown emoji', :js do + find('[data-name="thumbsdown"]').click + wait_for_requests + expect(thumbsdown_emoji).to have_text("1") + end + + it 'decrements the thumbsup emoji', :js do + expect(thumbsup_emoji).to have_text("0") + end + end + + it 'toggles the smiley emoji on a note', :js do + toggle_smiley_emoji(true) + + within('.note-body') do + expect(find(emoji_counter)).to have_text("1") + end + + toggle_smiley_emoji(false) + + within('.note-body') do + expect(page).not_to have_selector(emoji_counter) + end + end + + context 'execute /award quick action' do + it 'toggles the emoji award on noteable', :js do + execute_quick_action('/award :100:') + + expect(find(noteable_award_counter)).to have_text("1") + + execute_quick_action('/award :100:') + + expect(page).not_to have_selector(noteable_award_counter) + end + end + end + end + + context 'unauthorized user', :js do + before do + visit project_issue_path(project, issue) + end + + it 'has disabled emoji button' do + expect(first('.award-control')[:class]).to have_text('disabled') + end + end + + def execute_quick_action(cmd) + within('.js-main-target-form') do + fill_in 'note[note]', with: cmd + click_button 'Comment' + end + + wait_for_requests + end + + def thumbsup_emoji + page.all(emoji_counter).first + end + + def thumbsdown_emoji + page.all(emoji_counter).last + end + + def emoji_counter + 'span.js-counter' + end + + def noteable_award_counter + ".awards .active" + end + + def toggle_smiley_emoji(status) + within('.note') do + find('.note-emoji-button').click + end + + if !status + first('[data-name="smiley"]').click + else + find('[data-name="smiley"]').click + end + + wait_for_requests + end + end +end diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb deleted file mode 100644 index 4d860893abe0c6f8144f253414fda1f605fcbdf9..0000000000000000000000000000000000000000 --- a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb +++ /dev/null @@ -1,172 +0,0 @@ -require 'spec_helper' - -describe 'User interacts with awards in an issue', :js do - let(:issue) { create(:issue, project: project)} - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - project.add_maintainer(user) - sign_in(user) - - visit(project_issue_path(project, issue)) - end - - it 'toggles the thumbsup award emoji' do - page.within('.awards') do - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.js-emoji-btn') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.award-control.js-emoji-btn') - expect(page.all('.award-control.js-emoji-btn').size).to eq(2) - - page.all('.award-control.js-emoji-btn').each do |element| - expect(element['title']).to eq('') - end - - page.all('.award-control .js-counter').each do |element| - expect(element).to have_content('0') - end - - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.js-emoji-btn') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - end - end - - it 'toggles a custom award emoji' do - page.within('.awards') do - page.find('.js-add-award').click - end - - page.find('.emoji-menu.is-visible') - - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - - page.within('.emoji-menu-content') do - emoji_button = page.first('.js-emoji-btn') - emoji_button.hover - emoji_button.click - end - - page.within('.awards') do - expect(page).to have_selector('.js-emoji-btn') - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - - expect do - page.find('.js-emoji-btn.active').click - wait_for_requests - end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) - end - end - - it 'shows the list of award emoji categories' do - page.within('.awards') do - page.find('.js-add-award').click - end - - page.find('.emoji-menu.is-visible') - - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - - fill_in('emoji-menu-search', with: 'hand') - - page.within('.emoji-menu-content') do - expect(page).to have_selector('[data-name="raised_hand"]') - end - end - - it 'adds an award emoji by a comment' do - page.within('.js-main-target-form') do - fill_in('note[note]', with: ':smile:') - - click_button('Comment') - end - - expect(page).to have_emoji('smile') - end - - context 'when a project is archived' do - let(:project) { create(:project, :archived) } - - it 'hides the add award button' do - page.within('.awards') do - expect(page).not_to have_css('.js-add-award') - end - end - end - - context 'awards on a note' do - let!(:note) { create(:note, noteable: issue, project: issue.project) } - let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') } - - it 'shows the award on the note' do - page.within('.note-awards') do - expect(page).to have_emoji('100') - end - end - - it 'allows adding a vote to an award' do - page.within('.note-awards') do - find('gl-emoji[data-name="100"]').click - end - wait_for_requests - - expect(note.reload.award_emoji.size).to eq(2) - end - - it 'allows adding a new emoji' do - page.within('.note-actions') do - find('a.js-add-award').click - end - page.within('.emoji-menu-content') do - find('gl-emoji[data-name="8ball"]').click - end - wait_for_requests - - page.within('.note-awards') do - expect(page).to have_emoji('8ball') - end - expect(note.reload.award_emoji.size).to eq(2) - end - - context 'when the project is archived' do - let(:project) { create(:project, :archived) } - - it 'hides the buttons for adding new emoji' do - page.within('.note-awards') do - expect(page).not_to have_css('.award-menu-holder') - end - - page.within('.note-actions') do - expect(page).not_to have_css('a.js-add-award') - end - end - - it 'does not allow toggling existing emoji' do - page.within('.note-awards') do - find('gl-emoji[data-name="100"]').click - end - wait_for_requests - - expect(note.reload.award_emoji.size).to eq(1) - end - end - end -end diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 13175a9a3f76ea472a89f8e82d2bc57716a1f7dd..3a79ef996f5f04d368dacdaac5bf6a27a5755d59 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -210,6 +210,15 @@ describe('Board list component', () => { }); }); + it('does not load issues if already loading', () => { + component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {})); + + component.onScroll(); + component.onScroll(); + + expect(component.list.nextPage).toHaveBeenCalledTimes(1); + }); + it('shows loading more spinner', (done) => { component.showCount = true; component.list.loadingMore = true; diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 72eb20bdc873887bde31a6d54ff8291cb9247f8f..bca2033ff97612271ef52ee31ab32b8d559487cc 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -352,10 +352,22 @@ describe('IDE store file actions', () => { it('calls also getBaseRawFileData service method', done => { spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw')); + store.state.currentProjectId = 'gitlab-org/gitlab-ce'; + store.state.currentMergeRequestId = '1'; + store.state.projects = { + 'gitlab-org/gitlab-ce': { + mergeRequests: { + 1: { + baseCommitSha: 'SHA', + }, + }, + }, + }; + tmpFile.mrChange = { new_file: false }; store - .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' }) + .dispatch('getRawFileData', { path: tmpFile.path }) .then(() => { expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA'); expect(tmpFile.baseRaw).toBe('baseraw'); @@ -392,10 +404,7 @@ describe('IDE store file actions', () => { const dispatch = jasmine.createSpy('dispatch'); actions - .getRawFileData( - { state: store.state, commit() {}, dispatch }, - { path: tmpFile.path, baseSha: tmpFile.baseSha }, - ) + .getRawFileData({ state: store.state, commit() {}, dispatch }, { path: tmpFile.path }) .then(done.fail) .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { @@ -404,7 +413,6 @@ describe('IDE store file actions', () => { actionText: 'Please try again', actionPayload: { path: tmpFile.path, - baseSha: tmpFile.baseSha, }, }); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 1e836dbc3f90d2b7422bc2924ddda05f81980f27..6ce76aaa03b425b059256f48686dec3248da67c9 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -213,6 +213,33 @@ describe('Multi-file store mutations', () => { expect(localState.changedFiles).toEqual([localState.entries.filePath]); }); + + it('does not add tempFile into changedFiles', () => { + localState.entries.filePath = { + deleted: false, + type: 'blob', + tempFile: true, + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); + + it('removes tempFile from changedFiles when deleted', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + type: 'blob', + tempFile: true, + }; + + localState.changedFiles.push({ ...localState.entries.filePath }); + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); }); describe('UPDATE_FILE_AFTER_COMMIT', () => { diff --git a/spec/javascripts/jobs/commit_block_spec.js b/spec/javascripts/jobs/commit_block_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f755a5042b58aae4aee13516c3b8f26a500730a3 --- /dev/null +++ b/spec/javascripts/jobs/commit_block_spec.js @@ -0,0 +1,73 @@ +import Vue from 'vue'; +import component from '~/jobs/components/commit_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Commit block', () => { + const Component = Vue.extend(component); + let vm; + + const props = { + pipelineShortSha: '1f0fb84f', + pipelineShaPath: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', + mergeRequestReference: '!21244', + mergeRequestPath: 'merge_requests/21244', + gitCommitTitlte: 'Regenerate pot files', + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('pipeline short sha', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + }); + }); + + it('renders pipeline short sha link', () => { + expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(props.pipelineShaPath); + expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(props.pipelineShortSha); + }); + + it('renders clipboard button', () => { + expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(props.pipelineShortSha); + }); + }); + + describe('with merge request', () => { + it('renders merge request link and reference', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(props.mergeRequestPath); + expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(props.mergeRequestReference); + + }); + }); + + describe('without merge request', () => { + it('does not render merge request', () => { + const copyProps = Object.assign({}, props); + delete copyProps.mergeRequestPath; + delete copyProps.mergeRequestReference; + + vm = mountComponent(Component, { + ...copyProps, + }); + + expect(vm.$el.querySelector('.js-link-commit')).toBeNull(); + }); + }); + + describe('git commit title', () => { + it('renders git commit title', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.textContent).toContain(props.gitCommitTitlte); + }); + }); +}); diff --git a/spec/javascripts/jobs/components/job_log_controllers_spec.js b/spec/javascripts/jobs/components/job_log_controllers_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..416dfab8a48521ef1d55228feec8ad320926c54f --- /dev/null +++ b/spec/javascripts/jobs/components/job_log_controllers_spec.js @@ -0,0 +1,217 @@ +import Vue from 'vue'; +import component from '~/jobs/components/job_log_controllers.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Job log controllers', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('Truncate information', () => { + + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders size information', () => { + expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB'); + }); + + it('renders link to raw trace', () => { + expect(vm.$el.querySelector('.js-raw-link').getAttribute('href')).toEqual('/raw'); + }); + + }); + + describe('links section', () => { + describe('with raw trace path', () => { + it('renders raw trace link', () => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + + expect(vm.$el.querySelector('.js-raw-link-controller').getAttribute('href')).toEqual('/raw'); + }); + }); + + describe('without raw trace path', () => { + it('does not render raw trace link', () => { + vm = mountComponent(Component, { + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + + expect(vm.$el.querySelector('.js-raw-link-controller')).toBeNull(); + }); + }); + + describe('when is erasable', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders erase job button', () => { + expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull(); + }); + + describe('on click', () => { + describe('when user confirms action', () => { + it('emits eraseJob event', () => { + spyOn(window, 'confirm').and.returnValue(true); + spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-erase-link').click(); + + expect(vm.$emit).toHaveBeenCalledWith('eraseJob'); + }); + }); + + describe('when user does not confirm action', () => { + it('does not emit eraseJob event', () => { + spyOn(window, 'confirm').and.returnValue(false); + spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-erase-link').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('eraseJob'); + }); + }); + }); + }); + + describe('when it is not erasable', () => { + it('does not render erase button', () => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: false, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + + expect(vm.$el.querySelector('.js-erase-link')).toBeNull(); + }); + }); + }); + + describe('scroll buttons', () => { + describe('scroll top button', () => { + describe('when user can scroll top', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders enabled scroll top button', () => { + expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toBeNull(); + }); + + it('emits scrollJobLogTop event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-top').click(); + + expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogTop'); + }); + }); + + describe('when user can not scroll top', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: false, + canScrollToBottom: true, + }); + }); + + it('renders disabled scroll top button', () => { + expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toEqual('disabled'); + }); + + it('does not emit scrollJobLogTop event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-top').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogTop'); + }); + }); + }); + + describe('scroll bottom button', () => { + describe('when user can scroll bottom', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders enabled scroll bottom button', () => { + expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toBeNull(); + }); + + it('emits scrollJobLogBottom event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-bottom').click(); + + expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogBottom'); + }); + }); + + describe('when user can not scroll bottom', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: false, + }); + }); + + it('renders disabled scroll bottom button', () => { + expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toEqual('disabled'); + + }); + + it('does not emit scrollJobLogBottom event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-bottom').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogBottom'); + }); + }); + }); + }); +}); + diff --git a/spec/javascripts/jobs/empty_state_spec.js b/spec/javascripts/jobs/empty_state_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f8feb069fe01c655a6871b4f0bf4812e65ab5d00 --- /dev/null +++ b/spec/javascripts/jobs/empty_state_spec.js @@ -0,0 +1,90 @@ +import Vue from 'vue'; +import component from '~/jobs/components/empty_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Empty State', () => { + const Component = Vue.extend(component); + let vm; + + const props = { + illustrationPath: 'illustrations/pending_job_empty.svg', + illustrationSizeClass: 'svg-430', + title: 'This job has not started yet', + }; + + const content = 'This job is in pending state and is waiting to be picked by a runner'; + + afterEach(() => { + vm.$destroy(); + }); + + describe('renders image and title', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + content, + }); + }); + + it('renders img with provided path and size', () => { + expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath); + expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass); + }); + + it('renders provided title', () => { + expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual( + props.title, + ); + }); + }); + + describe('with content', () => { + it('renders content', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual( + content, + ); + }); + }); + + describe('without content', () => { + it('does not render content', () => { + vm = mountComponent(Component, { + ...props, + }); + expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull(); + }); + }); + + describe('with action', () => { + it('renders action', () => { + vm = mountComponent(Component, { + ...props, + content, + action: { + link: 'runner', + title: 'Check runner', + method: 'post', + }, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual( + 'runner', + ); + }); + }); + + describe('without action', () => { + it('does not render action', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/trigger_value_spec.js b/spec/javascripts/jobs/trigger_value_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..acf91510ed28a8fc3aa876d50253942df239a259 --- /dev/null +++ b/spec/javascripts/jobs/trigger_value_spec.js @@ -0,0 +1,66 @@ +import Vue from 'vue'; +import component from '~/jobs/components/trigger_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Trigger block', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('with short token', () => { + it('renders short token', () => { + vm = mountComponent(Component, { + shortToken: '0a666b2', + }); + + expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2'); + }); + }); + + describe('without short token', () => { + it('does not render short token', () => { + vm = mountComponent(Component, {}); + + expect(vm.$el.querySelector('.js-short-token')).toBeNull(); + }); + }); + + describe('with variables', () => { + describe('reveal variables', () => { + it('reveals variables on click', done => { + vm = mountComponent(Component, { + variables: { + key: 'value', + variable: 'foo', + }, + }); + + vm.$el.querySelector('.js-reveal-variables').click(); + + vm + .$nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull(); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('key'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('value'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('variable'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('foo'); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('without variables', () => { + it('does not render variables', () => { + vm = mountComponent(Component); + + expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull(); + expect(vm.$el.querySelector('.js-build-variables')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js index 911c09e5b238db6d1e7743ebe427953ae9666d2b..adb5ff682f05e938ec2aa9f87324b787879d72da 100644 --- a/spec/javascripts/vue_shared/translate_spec.js +++ b/spec/javascripts/vue_shared/translate_spec.js @@ -202,11 +202,11 @@ describe('Vue translate filter', () => { <span> {{ n__( \` - multiline + multiline string \`, \` - multiline + multiline strings \`, 2 @@ -234,7 +234,7 @@ describe('Vue translate filter', () => { {{ s__( \` Context| - multiline + multiline string \` ) }} diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 749b20947878941a14287473a19920c98c8bb19f..797d767465a6980b5ba67b693d0f393e6ce3353e 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -100,7 +100,7 @@ def domain_variable end end - describe '#set_gitlab_deploy_token' do + describe '#create_gitlab_deploy_token' do let(:auto_devops) { build(:project_auto_devops, project: project) } context 'when the project is public' do @@ -144,9 +144,9 @@ def domain_variable end end - context 'when autodevops is enabled at instancel level' do + context 'when autodevops is enabled at instance level' do let(:project) { create(:project, :repository, :internal) } - let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) } + let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) } it 'should create a deploy token' do allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index fabe52e0b6dac3aaf3327144e848ab68cfba5c02..8a91c0f10eb2bcd9b95fd78cd69719aa6294f405 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3596,6 +3596,11 @@ def enable_lfs end describe '#auto_devops_enabled?' do + before do + allow(Feature).to receive(:enabled?).and_call_original + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0) + end + set(:project) { create(:project) } subject { project.auto_devops_enabled? } @@ -3605,19 +3610,14 @@ def enable_lfs stub_application_setting(auto_devops_enabled: true) end - it 'auto devops is implicitly enabled' do - expect(project.auto_devops).to be_nil - expect(project).to be_auto_devops_enabled - end + it { is_expected.to be_truthy } context 'when explicitly enabled' do before do create(:project_auto_devops, project: project) end - it "auto devops is enabled" do - expect(project).to be_auto_devops_enabled - end + it { is_expected.to be_truthy } end context 'when explicitly disabled' do @@ -3625,9 +3625,7 @@ def enable_lfs create(:project_auto_devops, project: project, enabled: false) end - it "auto devops is disabled" do - expect(project).not_to be_auto_devops_enabled - end + it { is_expected.to be_falsey } end end @@ -3636,19 +3634,22 @@ def enable_lfs stub_application_setting(auto_devops_enabled: false) end - it 'auto devops is implicitly disabled' do - expect(project.auto_devops).to be_nil - expect(project).not_to be_auto_devops_enabled - end + it { is_expected.to be_falsey } context 'when explicitly enabled' do before do create(:project_auto_devops, project: project) end - it "auto devops is enabled" do - expect(project).to be_auto_devops_enabled + it { is_expected.to be_truthy } + end + + context 'when force_autodevops_on_by_default is enabled for the project' do + before do + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100) end + + it { is_expected.to be_truthy } end end end @@ -3698,6 +3699,11 @@ def enable_lfs end describe '#has_auto_devops_implicitly_disabled?' do + before do + allow(Feature).to receive(:enabled?).and_call_original + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0) + end + set(:project) { create(:project) } context 'when enabled in settings' do @@ -3719,6 +3725,16 @@ def enable_lfs expect(project).to have_auto_devops_implicitly_disabled end + context 'when force_autodevops_on_by_default is enabled for the project' do + before do + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100) + end + + it 'does not have auto devops implicitly disabled' do + expect(project).not_to have_auto_devops_implicitly_disabled + end + end + context 'when explicitly disabled' do before do create(:project_auto_devops, project: project, enabled: false) diff --git a/spec/services/projects/detect_repository_languages_service_spec.rb b/spec/services/projects/detect_repository_languages_service_spec.rb index f90d558938f0d8afae52ceec48465520e8bb547d..deea1189cdf3dfd8d4afcf7d52e6164e898d7529 100644 --- a/spec/services/projects/detect_repository_languages_service_spec.rb +++ b/spec/services/projects/detect_repository_languages_service_spec.rb @@ -5,10 +5,6 @@ subject { described_class.new(project, project.owner) } - before do - allow(Feature).to receive(:disabled?).and_return(false) - end - describe '#execute' do context 'without previous detection' do it 'inserts new programming languages in the database' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 198499237594ff88f2cef57fc0289f5ab69e7252..a819f26a0c2a52ba4b3101d07048117d3e4e81a4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -114,6 +114,13 @@ config.before(:example) do # Enable all features by default for testing allow(Feature).to receive(:enabled?) { true } + + # The following can be removed when we remove the staged rollout strategy + # and we can just enable it using instance wide settings + # (ie. ApplicationSetting#auto_devops_enabled) + allow(Feature).to receive(:enabled?) + .with(:force_autodevops_on_by_default, anything) + .and_return(false) end config.before(:example, :request_store) do diff --git a/spec/tasks/gitlab/traces_rake_spec.rb b/spec/tasks/gitlab/traces_rake_spec.rb index bd18e8ffc1e1b1b017867ed51a7e9e34e3b6eadf..aaf0d7242dd50c97ff1ffa51d92ce320bdd49056 100644 --- a/spec/tasks/gitlab/traces_rake_spec.rb +++ b/spec/tasks/gitlab/traces_rake_spec.rb @@ -5,51 +5,109 @@ Rake.application.rake_require 'tasks/gitlab/traces' end - shared_examples 'passes the job id to worker' do - it do - expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]]) + describe 'gitlab:traces:archive' do + shared_examples 'passes the job id to worker' do + it do + expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]]) - run_rake_task('gitlab:traces:archive') + run_rake_task('gitlab:traces:archive') + end end - end - shared_examples 'does not pass the job id to worker' do - it do - expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async) + shared_examples 'does not pass the job id to worker' do + it do + expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async) - run_rake_task('gitlab:traces:archive') + run_rake_task('gitlab:traces:archive') + end end - end - context 'when trace file stored in default path' do - let!(:job) { create(:ci_build, :success, :trace_live) } + context 'when trace file stored in default path' do + let!(:job) { create(:ci_build, :success, :trace_live) } - it_behaves_like 'passes the job id to worker' - end + it_behaves_like 'passes the job id to worker' + end - context 'when trace is stored in database' do - let!(:job) { create(:ci_build, :success) } + context 'when trace is stored in database' do + let!(:job) { create(:ci_build, :success) } - before do - job.update_column(:trace, 'trace in db') + before do + job.update_column(:trace, 'trace in db') + end + + it_behaves_like 'passes the job id to worker' end - it_behaves_like 'passes the job id to worker' + context 'when job has trace artifact' do + let!(:job) { create(:ci_build, :success) } + + before do + create(:ci_job_artifact, :trace, job: job) + end + + it_behaves_like 'does not pass the job id to worker' + end + + context 'when job is not finished yet' do + let!(:build) { create(:ci_build, :running, :trace_live) } + + it_behaves_like 'does not pass the job id to worker' + end end - context 'when job has trace artifact' do - let!(:job) { create(:ci_build, :success) } + describe 'gitlab:traces:migrate' do + let(:object_storage_enabled) { false } before do - create(:ci_job_artifact, :trace, job: job) + stub_artifacts_object_storage(enabled: object_storage_enabled) end - it_behaves_like 'does not pass the job id to worker' - end + subject { run_rake_task('gitlab:traces:migrate') } - context 'when job is not finished yet' do - let!(:build) { create(:ci_build, :running, :trace_live) } + let!(:job_trace) { create(:ci_job_artifact, :trace, file_store: store) } - it_behaves_like 'does not pass the job id to worker' + context 'when local storage is used' do + let(:store) { ObjectStorage::Store::LOCAL } + + context 'and job does not have file store defined' do + let(:object_storage_enabled) { true } + let(:store) { nil } + + it "migrates file to remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'and remote storage is defined' do + let(:object_storage_enabled) { true } + + it "migrates file to remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'and remote storage is not defined' do + it "fails to migrate to remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end + + context 'when remote storage is used' do + let(:object_storage_enabled) { true } + let(:store) { ObjectStorage::Store::REMOTE } + + it "file stays on remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end end end