summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2018-05-18 17:25:24 +0100
committerFilipa Lacerda <filipa@gitlab.com>2018-05-18 17:25:24 +0100
commit7f0f8594d3e5cbee41771c476e3b7496946db300 (patch)
tree1981c15675409386908b3b05d4f1aa75b1755957
parent068186555cb85e85bbfe04afe858fb3eb4801207 (diff)
parent8bacfbd1cc4f6c3678d50dd516df2b59eb0c8864 (diff)
downloadgitlab-ce-5845-extract-ee-environments-files.tar.gz
Merge branch 'master' into 5845-extract-ee-environments-files5845-extract-ee-environments-files
* master: (69 commits) Calculating repository checksums executed by Gitaly Resolve "Expand API: Render an arbitrary Markdown document" Update EE > CE downgrade service removal steps Make stores export a createStore() which can be used in tests Simplify pattern lexeme fabrication and matcher Simplify untrusted regexp factory method Fix api_json.log not always reporting the right HTTP status code Move group lists css from framework/lists.scss to pages/groups.scss Resolve "Web IDE: Previewing Markdown in Firefox doesn’t show a scroll bar" Add Keyboard shortcuts for "Kubernetes" and "Environments" Move API group deletion to Sidekiq fix typos. add a reference to deliverable and stretch for design artifact fix / assigne username wrapping problem has been fixed Memoize Gitlab::Database.version Conditionally add Gitaly deprecation warnings based on ENV variable Bring CE-EE parity to app/services/milestones/base_service.rb Bring CE-EE parity to app/services/lfs/unlock_file_service.rb Fixes 500 error on /estimate BIG_VALUE Fix: Use case in-sensitive ordering by name for groups Fix group lists visual ...
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--CONTRIBUTING.md21
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue4
-rw-r--r--app/assets/javascripts/ide/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue55
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue19
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js30
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js5
-rw-r--r--app/assets/stylesheets/framework/lists.scss237
-rw-r--r--app/assets/stylesheets/pages/groups.scss232
-rw-r--r--app/assets/stylesheets/pages/issuable.scss18
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss58
-rw-r--r--app/assets/stylesheets/pages/repo.scss114
-rw-r--r--app/controllers/boards/issues_controller.rb2
-rw-r--r--app/controllers/concerns/accepts_pending_invitations.rb15
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb25
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb9
-rw-r--r--app/controllers/registrations_controller.rb4
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/pipeline.rb13
-rw-r--r--app/models/commit.rb28
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/concerns/time_trackable.rb4
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/user.rb10
-rw-r--r--app/presenters/ci/build_presenter.rb25
-rw-r--r--app/presenters/commit_status_presenter.rb24
-rw-r--r--app/presenters/generic_commit_status_presenter.rb2
-rw-r--r--app/serializers/pipeline_entity.rb6
-rw-r--r--app/services/keys/base_service.rb2
-rw-r--r--app/services/keys/destroy_service.rb12
-rw-r--r--app/services/lfs/unlock_file_service.rb8
-rw-r--r--app/services/milestones/base_service.rb1
-rw-r--r--app/views/help/_shortcuts.html.haml14
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/notify/member_invited_email.html.haml2
-rw-r--r--changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml5
-rw-r--r--changelogs/unreleased/42531-open-invite-404.yml5
-rw-r--r--changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml5
-rw-r--r--changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml5
-rw-r--r--changelogs/unreleased/46193-fix-big-estimate.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml5
-rw-r--r--changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml5
-rw-r--r--changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml5
-rw-r--r--changelogs/unreleased/ccr-incoming-email-regex-anchor.yml3
-rw-r--r--changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml5
-rw-r--r--changelogs/unreleased/fix-assignee-name-wrap.yml5
-rw-r--r--changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml5
-rw-r--r--changelogs/unreleased/memoize-database-version.yml5
-rw-r--r--changelogs/unreleased/pipelines-index-performance.yml5
-rw-r--r--changelogs/unreleased/sh-fix-grape-logging-status-code.yml5
-rw-r--r--changelogs/unreleased/sh-move-delete-groups-api-async.yml5
-rw-r--r--changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml5
-rw-r--r--changelogs/unreleased/zj-calculate-checksum-mandator.yml5
-rw-r--r--config/initializers/deprecations.rb2
-rw-r--r--db/migrate/20180504195842_project_name_lower_index.rb32
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/groups.md3
-rw-r--r--doc/api/markdown.md29
-rw-r--r--doc/ci/variables/README.md10
-rw-r--r--doc/ci/yaml/README.md13
-rw-r--r--doc/development/fe_guide/vuex.md8
-rw-r--r--doc/downgrade_ee_to_ce/README.md21
-rw-r--r--doc/raketasks/backup_restore.md35
-rw-r--r--doc/topics/autodevops/quick_start_guide.md4
-rw-r--r--doc/workflow/shortcuts.md10
-rw-r--r--lib/api/api.rb18
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/markdown.rb33
-rw-r--r--lib/api/v3/groups.rb5
-rw-r--r--lib/banzai/filter/reference_filter.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/gitlab/ci/pipeline/expression.rb10
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb29
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb33
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb8
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb9
-rw-r--r--lib/gitlab/ci/pipeline/preloader.rb28
-rw-r--r--lib/gitlab/database.rb2
-rw-r--r--lib/gitlab/git/repository.rb44
-rw-r--r--lib/gitlab/incoming_email.rb2
-rw-r--r--lib/gitlab/untrusted_regexp.rb26
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--qa/qa/specs/features/merge_request/create_spec.rb2
-rw-r--r--scripts/create_mysql_user.sh1
-rw-r--r--scripts/create_postgres_user.sh4
-rw-r--r--scripts/prepare_build.sh18
-rw-r--r--scripts/utils.sh18
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb14
-rw-r--r--spec/features/invites_spec.rb112
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb41
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js71
-rw-r--r--spec/javascripts/pipelines/stage_spec.js27
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb80
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb96
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb99
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/token_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/preloader_spec.rb20
-rw-r--r--spec/lib/gitlab/database_spec.rb14
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb72
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb4
-rw-r--r--spec/lib/gitlab/untrusted_regexp_spec.rb45
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb40
-rw-r--r--spec/models/ci/pipeline_spec.rb27
-rw-r--r--spec/models/commit_spec.rb84
-rw-r--r--spec/models/commit_status_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb13
-rw-r--r--spec/models/concerns/sortable_spec.rb108
-rw-r--r--spec/models/generic_commit_status_spec.rb6
-rw-r--r--spec/models/user_spec.rb18
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb2
-rw-r--r--spec/presenters/commit_status_presenter_spec.rb15
-rw-r--r--spec/requests/api/groups_spec.rb9
-rw-r--r--spec/requests/api/markdown_spec.rb112
-rw-r--r--spec/requests/api/v3/groups_spec.rb8
-rw-r--r--spec/serializers/pipeline_entity_spec.rb7
-rw-r--r--spec/services/keys/destroy_service_spec.rb13
-rw-r--r--spec/support/shared_examples/malicious_regexp_shared_examples.rb2
135 files changed, 2125 insertions, 726 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b1445feee58..84d8e69b84e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -189,7 +189,7 @@ stages:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
- CREATE_DB_USER: "true"
+ SETUP_DB: "false"
script:
# Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
@@ -233,7 +233,7 @@ stages:
.migration-paths: &migration-paths
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
- CREATE_DB_USER: "true"
+ SETUP_DB: "false"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
@@ -242,7 +242,7 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date
- - git checkout $CI_COMMIT_SHA
+ - git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- . scripts/prepare_build.sh
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5447ebbdd8c..383d13656e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -168,7 +168,7 @@ hits. They are not always necessary, but very convenient.
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
-issue is labelled with a subject label corresponding to your expertise.
+issue is labeled with a subject label corresponding to your expertise.
Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
@@ -296,7 +296,24 @@ any potential community contributor to @-mention per above.
## Implement design & UI elements
-Please see the [UX Guide for GitLab].
+For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
+
+The UX team uses labels to manage their workflow.
+
+The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
+To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook.
+
+Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue.
+
+The UX team has a special type label called ~"design artifact". This label indicates that the final output
+for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
+Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
+needed until the solution has been decided.
+
+~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
+
+Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the
+existing issue or decide to create a whole new issue for the purpose of development.
## Issue tracker
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 4a645c827ad..81961fe3c57 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -5,7 +5,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
-import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
+import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
export default {
components: {
@@ -70,7 +70,7 @@ export default {
? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
- this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ this.componentHeight = elHeight;
},
enterTransition() {
this.$nextTick(() => {
@@ -78,7 +78,7 @@ export default {
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
- this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
+ this.componentHeight = elHeight;
});
},
afterEndTransition() {
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 14946f8c9fa..7bc865058c6 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -122,11 +122,11 @@ export default {
<div
class="file"
:class="fileClass"
+ @click="clickFile"
+ role="button"
>
<div
class="file-name"
- @click="clickFile"
- role="button"
>
<span
class="ide-file-name str-truncated"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 48d4cc43198..83fe22f40a4 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -5,8 +5,6 @@ export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
-export const COMMIT_ITEM_PADDING = 32;
-
// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index fd3491c7fe0..82b4ce083fb 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,11 +1,21 @@
<script>
import $ from 'jquery';
-import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { dasherize } from '../../../lib/utils/text_utility';
-import eventHub from '../../event_hub';
+import axios from '~/lib/utils/axios_utils';
+import { dasherize } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+
/**
- * Renders either a cancel, retry or play icon pointing to the given path.
+ * Renders either a cancel, retry or play icon button and handles the post request
+ *
+ * Used in:
+ * - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
+ * - pipelines table
+ * - pipelines table in merge request page
+ * - pipelines table in commit page
+ * - pipelines detail page in big graph
*/
export default {
components: {
@@ -32,16 +42,10 @@ export default {
required: true,
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
data() {
return {
isDisabled: false,
- linkRequested: '',
};
},
@@ -51,19 +55,28 @@ export default {
return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
- watch: {
- requestFinishedFor() {
- if (this.requestFinishedFor === this.linkRequested) {
- this.isDisabled = false;
- }
- },
- },
methods: {
+ /**
+ * The request should not be handled here.
+ * However due to this component being used in several
+ * different apps it avoids repetition & complexity.
+ *
+ */
onClickAction() {
$(this.$el).tooltip('hide');
- eventHub.$emit('postAction', this.link);
- this.linkRequested = this.link;
+
this.isDisabled = true;
+
+ axios.post(`${this.link}.json`)
+ .then(() => {
+ this.isDisabled = false;
+ this.$emit('pipelineActionRequestComplete');
+ })
+ .catch(() => {
+ this.isDisabled = false;
+
+ createFlash(__('An error occurred while making the request.'));
+ });
},
},
};
@@ -80,6 +93,6 @@ btn-transparent ci-action-icon-container ci-action-icon-wrapper"
data-container="body"
:disabled="isDisabled"
>
- <icon :name="actionIcon" />
+ <icon :name="actionIcon"/>
</button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 4027d26098f..7bfe11ab8cd 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -42,11 +42,6 @@ export default {
type: Object,
required: true,
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
@@ -76,11 +71,15 @@ export default {
e.stopPropagation();
});
},
+
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
+ },
},
};
</script>
<template>
- <div class="ci-job-dropdown-container">
+ <div class="ci-job-dropdown-container dropdown">
<button
v-tooltip
type="button"
@@ -110,7 +109,7 @@ export default {
<job-component
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
- :request-finished-for="requestFinishedFor"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 7b8a5edcbff..4ec67f6c01b 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -16,11 +16,6 @@ export default {
type: Object,
required: true,
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
@@ -51,6 +46,10 @@ export default {
return className;
},
+
+ refreshPipelineGraph() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -74,7 +73,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
- :request-finished-for="requestFinishedFor"
+ @refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index c1f0f051b63..27b938c4985 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -46,11 +46,6 @@ export default {
required: false,
default: '',
},
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
status() {
@@ -84,6 +79,11 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
+ methods: {
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
+ },
+ },
};
</script>
<template>
@@ -126,7 +126,7 @@ export default {
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
- :request-finished-for="requestFinishedFor"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 5461fdbbadd..f32368947e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -29,12 +29,6 @@ export default {
required: false,
default: '',
},
-
- requestFinishedFor: {
- type: String,
- required: false,
- default: '',
- },
},
methods: {
@@ -49,6 +43,10 @@ export default {
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
+
+ pipelineActionRequestComplete() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -75,12 +73,13 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
- :request-finished-for="requestFinishedFor"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 498a97851fa..0f671ceea21 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -9,6 +9,7 @@
import CommitComponent from '../../vue_shared/components/commit.vue';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
+ import { PIPELINES_TABLE } from '../constants';
/**
* Pipeline table row.
@@ -46,6 +47,7 @@
required: true,
},
},
+ pipelinesTable: PIPELINES_TABLE,
data() {
return {
isRetrying: false,
@@ -297,6 +299,7 @@
v-for="(stage, index) in pipeline.details.stages"
:key="index">
<pipeline-stage
+ :type="$options.pipelinesTable"
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index a65485c05eb..f9769815796 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
+import { PIPELINES_TABLE } from '../constants';
export default {
components: {
@@ -44,6 +45,12 @@ export default {
required: false,
default: false,
},
+
+ type: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
@@ -133,6 +140,16 @@ export default {
isDropdownOpen() {
return this.$el.classList.contains('open');
},
+
+ pipelineActionRequestComplete() {
+ if (this.type === PIPELINES_TABLE) {
+ // warn the table to update
+ eventHub.$emit('refreshPipelinesTable');
+ } else {
+ // close the dropdown in mr widget
+ $(this.$refs.dropdown).dropdown('toggle');
+ }
+ },
},
};
</script>
@@ -151,6 +168,7 @@ export default {
id="stageDropdown"
aria-haspopup="true"
aria-expanded="false"
+ ref="dropdown"
>
<span
@@ -188,6 +206,7 @@ export default {
<job-component
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index b384c7500e7..eaa11a84cb9 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -1,2 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
+export const PIPELINES_TABLE = 'PIPELINES_TABLE';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index de0faf181e5..30b1eee186d 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -55,11 +55,13 @@ export default {
eventHub.$on('postAction', this.postAction);
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
+ eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
+ eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
},
destroyed() {
this.poll.stop();
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 04fe7958fe6..b49a16a87e6 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -25,30 +25,14 @@ export default () => {
data() {
return {
mediator,
- requestFinishedFor: null,
};
},
- created() {
- eventHub.$on('postAction', this.postAction);
- },
- beforeDestroy() {
- eventHub.$off('postAction', this.postAction);
- },
methods: {
- postAction(action) {
- // Click was made, reset this variable
- this.requestFinishedFor = null;
-
- this.mediator.service
- .postAction(action)
- .then(() => {
- this.mediator.refreshPipeline();
- this.requestFinishedFor = action;
- })
- .catch(() => {
- this.requestFinishedFor = action;
- Flash(__('An error occurred while making the request.'));
- });
+ requestRefreshPipelineGraph() {
+ // When an action is clicked
+ // (wether in the dropdown or in the main nodes, we refresh the big graph)
+ this.mediator.refreshPipeline()
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
render(createElement) {
@@ -56,7 +40,9 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
- requestFinishedFor: this.requestFinishedFor,
+ },
+ on: {
+ refreshPipelineGraph: this.requestRefreshPipelineGraph,
},
});
},
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index a4d10850471..78f7353eb0d 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts {
super();
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
- Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
+ Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
- Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
+ Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
+ Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index f1a8a46dda4..45517416e93 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -279,251 +279,14 @@ ul.indent-list {
padding: 10px 0 0 30px;
}
-
// Specific styles for tree list
@keyframes spin-avatar {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
-.groups-list-tree-container {
- .has-no-search-results {
- text-align: center;
- padding: $gl-padding;
- font-style: italic;
- color: $well-light-text-color;
- }
-
- > .group-list-tree > .group-row.has-children:first-child {
- border-top: 0;
- }
-}
-
-.group-list-tree {
- .avatar-container.content-loading {
- position: relative;
-
- > a,
- > a .avatar {
- height: 100%;
- border-radius: 50%;
- }
-
- > a {
- padding: 2px;
-
- .avatar {
- border: 2px solid $white-normal;
-
- &.identicon {
- line-height: 15px;
- }
- }
- }
-
- &::after {
- content: "";
- position: absolute;
- height: 100%;
- width: 100%;
- background-color: transparent;
- border: 2px outset $kdb-border;
- border-radius: 50%;
- animation: spin-avatar 3s infinite linear;
- }
- }
-
- .folder-toggle-wrap {
- float: left;
- line-height: $list-text-height;
- font-size: 0;
-
- span {
- font-size: $gl-font-size;
- }
- }
-
- .folder-caret,
- .item-type-icon {
- display: inline-block;
- }
-
- .folder-caret {
- width: 15px;
-
- svg {
- margin-bottom: 2px;
- }
- }
-
- .item-type-icon {
- margin-top: 2px;
- width: 20px;
- }
-
- > .group-row:not(.has-children) {
- .folder-caret {
- opacity: 0;
- }
- }
-
- .content-list li:last-child {
- padding-bottom: 0;
- }
-
- .group-list-tree {
- margin-bottom: 0;
- margin-left: 30px;
- position: relative;
-
- &::before {
- content: '';
- display: block;
- width: 0;
- position: absolute;
- top: 5px;
- bottom: 0;
- left: -16px;
- border-left: 2px solid $border-white-normal;
- }
-
- .group-row {
- position: relative;
-
- &::before {
- content: "";
- display: block;
- width: 10px;
- height: 0;
- border-top: 2px solid $border-white-normal;
- position: absolute;
- top: 30px;
- left: -16px;
- }
-
- &:last-child::before {
- background: $white-light;
- height: auto;
- top: 30px;
- bottom: 0;
- }
-
- &.being-removed {
- opacity: 0.5;
- }
- }
- }
-
- .group-row {
- padding: 0;
-
- &.has-children {
- border-top: 0;
- }
-
- &:first-child {
- border-top: 1px solid $white-normal;
- }
-
- &:last-of-type {
- .group-row-contents:not(:hover) {
- border-bottom: 1px solid transparent;
- }
- }
- }
-
- .group-row-contents {
- padding: 10px 10px 8px;
- border-top: solid 1px transparent;
- border-bottom: solid 1px $white-normal;
-
- &:hover {
- border-color: $row-hover-border;
- background-color: $row-hover;
- cursor: pointer;
- }
-
- .avatar-container > a {
- width: 100%;
- text-decoration: none;
- }
-
- &.has-more-items {
- display: block;
- padding: 20px 10px;
- }
-
- .stats {
- position: relative;
- line-height: 46px;
-
- > span {
- display: inline-flex;
- align-items: center;
- height: 16px;
- min-width: 30px;
- }
-
- > span:last-child {
- margin-right: 0;
- }
-
- .stat-value {
- margin: 2px 0 0 5px;
- }
- }
-
- .controls {
- margin-left: 5px;
-
- > .btn {
- margin-right: $btn-xs-side-margin;
- }
- }
- }
-
- .project-row-contents .stats {
- line-height: inherit;
-
- > span:first-child {
- margin-left: 25px;
- }
-
- .item-visibility {
- margin-right: 0;
- }
-
- .last-updated {
- position: absolute;
- right: 12px;
- min-width: 250px;
- text-align: right;
- color: $gl-text-color-secondary;
- }
- }
-}
-
.namespace-title {
.tooltip-inner {
max-width: 350px;
}
}
-
-ul.group-list-tree {
- li.group-row {
- > .group-row-contents .title {
- line-height: $list-text-height;
- }
-
- &.has-description > .group-row-contents .title {
- line-height: inherit;
- }
- }
-}
-
-.js-groups-list-holder {
- .groups-list-loading {
- font-size: 34px;
- text-align: center;
- }
-}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 6ee8b33bd39..409b7285f82 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -18,6 +18,10 @@
.group-row {
@include basic-list-stats;
+
+ .description p {
+ margin-bottom: 0;
+ }
}
.ldap-group-links {
@@ -237,3 +241,231 @@
overflow-y: unset;
}
}
+
+.groups-list-tree-container {
+ .has-no-search-results {
+ text-align: center;
+ padding: $gl-padding;
+ font-style: italic;
+ color: $well-light-text-color;
+ }
+
+ > .group-list-tree > .group-row.has-children:first-child {
+ border-top: 0;
+ }
+}
+
+.group-list-tree {
+ .avatar-container.content-loading {
+ position: relative;
+
+ > a,
+ > a .avatar {
+ height: 100%;
+ border-radius: 50%;
+ }
+
+ > a {
+ padding: 2px;
+
+ .avatar {
+ border: 2px solid $white-normal;
+
+ &.identicon {
+ line-height: 15px;
+ }
+ }
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background-color: transparent;
+ border: 2px outset $kdb-border;
+ border-radius: 50%;
+ animation: spin-avatar 3s infinite linear;
+ }
+ }
+
+ .folder-toggle-wrap {
+ float: left;
+ line-height: $list-text-height;
+ font-size: 0;
+
+ span {
+ font-size: $gl-font-size;
+ }
+ }
+
+ .folder-caret,
+ .item-type-icon {
+ display: inline-block;
+ }
+
+ .folder-caret {
+ width: 15px;
+
+ svg {
+ margin-bottom: 2px;
+ }
+ }
+
+ .item-type-icon {
+ margin-top: 2px;
+ width: 20px;
+ }
+
+ > .group-row:not(.has-children) {
+ .folder-caret {
+ opacity: 0;
+ }
+ }
+
+ .content-list li:last-child {
+ padding-bottom: 0;
+ }
+
+ .group-list-tree {
+ margin-bottom: 0;
+ margin-left: 30px;
+ position: relative;
+
+ &::before {
+ content: '';
+ display: block;
+ width: 0;
+ position: absolute;
+ top: 5px;
+ bottom: 0;
+ left: -16px;
+ border-left: 2px solid $border-white-normal;
+ }
+
+ .group-row {
+ position: relative;
+
+ &::before {
+ content: "";
+ display: block;
+ width: 10px;
+ height: 0;
+ border-top: 2px solid $border-white-normal;
+ position: absolute;
+ top: 30px;
+ left: -16px;
+ }
+
+ &:last-child::before {
+ background: $white-light;
+ height: auto;
+ top: 30px;
+ bottom: 0;
+ }
+
+ &.being-removed {
+ opacity: 0.5;
+ }
+ }
+ }
+
+ .group-row {
+ padding: 0;
+
+ &.has-children {
+ border-top: 0;
+ }
+
+ &:first-child {
+ border-top: 1px solid $white-normal;
+ }
+ }
+
+ .group-row-contents {
+ padding: $gl-padding-top;
+
+ &:hover {
+ border-color: $row-hover-border;
+ background-color: $row-hover;
+ cursor: pointer;
+ }
+
+ .avatar-container > a {
+ width: 100%;
+ text-decoration: none;
+ }
+
+ &.has-more-items {
+ display: block;
+ padding: 20px 10px;
+ }
+
+ .stats {
+ position: relative;
+ line-height: 46px;
+
+ > span {
+ display: inline-flex;
+ align-items: center;
+ height: 16px;
+ min-width: 30px;
+ }
+
+ > span:last-child {
+ margin-right: 0;
+ }
+
+ .stat-value {
+ margin: 2px 0 0 5px;
+ }
+ }
+
+ .controls {
+ margin-left: 5px;
+
+ > .btn {
+ margin-right: $btn-xs-side-margin;
+ }
+ }
+ }
+
+ .project-row-contents .stats {
+ line-height: inherit;
+
+ > span:first-child {
+ margin-left: 25px;
+ }
+
+ .item-visibility {
+ margin-right: 0;
+ }
+
+ .last-updated {
+ position: absolute;
+ right: 12px;
+ min-width: 250px;
+ text-align: right;
+ color: $gl-text-color-secondary;
+ }
+ }
+}
+
+ul.group-list-tree {
+ li.group-row {
+ > .group-row-contents .title {
+ line-height: $list-text-height;
+ }
+
+ &.has-description > .group-row-contents .title {
+ line-height: inherit;
+ }
+ }
+}
+
+.js-groups-list-holder {
+ .groups-list-loading {
+ font-size: 34px;
+ text-align: center;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index b2dad4a358a..a8110f069d4 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -197,9 +197,21 @@
}
&.assignee {
- .author_link:hover {
- .author {
- text-decoration: underline;
+ .author_link {
+ display: block;
+ padding-left: 42px;
+ position: relative;
+
+ &:hover {
+ .author {
+ text-decoration: underline;
+ }
+ }
+
+ .avatar {
+ left: 0;
+ position: absolute;
+ top: 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 02803e7b040..1264d977b2f 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -66,13 +66,9 @@
}
}
- .btn-group {
- &.open {
- .btn-default {
- background-color: $white-normal;
- border-color: $border-white-normal;
- }
- }
+ .btn-group.open .btn-default {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
}
.btn .text-center {
@@ -361,16 +357,14 @@
&:not(:first-child) {
margin-left: 44px;
- .left-connector {
- &::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -44px;
- border-top: 2px solid $border-color;
- width: 44px;
- height: 1px;
- }
+ .left-connector::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -44px;
+ border-top: 2px solid $border-color;
+ width: 44px;
+ height: 1px;
}
}
}
@@ -386,22 +380,16 @@
&:last-child {
.build {
// Remove right connecting horizontal line from first build in last stage
- &:first-child {
- &::after {
- border: 0;
- }
+ &:first-child::after {
+ border: 0;
}
// Remove right curved connectors from all builds in last stage
- &:not(:first-child) {
- &::after {
- border: 0;
- }
+ &:not(:first-child)::after {
+ border: 0;
}
// Remove opposite curve
- .curve {
- &::before {
- display: none;
- }
+ .curve::before {
+ display: none;
}
}
}
@@ -409,16 +397,12 @@
&:first-child {
.build {
// Remove left curved connectors from all builds in first stage
- &:not(:first-child) {
- &::before {
- border: 0;
- }
+ &:not(:first-child)::before {
+ border: 0;
}
// Remove opposite curve
- .curve {
- &::after {
- display: none;
- }
+ .curve::after {
+ display: none;
}
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 00457717f00..175d2779bb7 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -39,12 +39,15 @@
.ide-file-list {
flex: 1;
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+ padding-bottom: $grid-size;
.file {
cursor: pointer;
&.file-open {
- background: $link-active-background;
+ background: $white-normal;
}
&.file-active {
@@ -84,12 +87,11 @@
.ide-new-btn {
display: none;
- margin-right: -8px;
}
&:hover,
&:focus {
- background: $link-active-background;
+ background: $white-normal;
.ide-new-btn {
display: block;
@@ -111,12 +113,11 @@
}
}
-.file-name,
-.file-col-commit-message {
+.file-name {
display: flex;
overflow: visible;
align-items: center;
- padding: 6px 12px;
+ width: 100%;
}
.multi-file-loading-container {
@@ -306,8 +307,18 @@
}
.preview-container {
- height: 100%;
- overflow: auto;
+ flex-grow: 1;
+ position: relative;
+
+ .md-previewer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ padding: $gl-padding;
+ }
.file-container {
background-color: $gray-darker;
@@ -347,10 +358,6 @@
color: $diff-image-info-color;
}
}
-
- .md-previewer {
- padding: $gl-padding;
- }
}
.ide-mode-tabs {
@@ -501,7 +508,7 @@
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding $gl-padding;
+ padding: 12px 0;
}
.multi-file-commit-panel-header-title {
@@ -523,32 +530,31 @@
.multi-file-commit-list {
flex: 1;
overflow: auto;
- padding: $gl-padding;
+ padding: $grid-size 0;
+ margin-left: -$grid-size;
+ margin-right: -$grid-size;
min-height: 60px;
+
+ .multi-file-commit-list-item {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ &.help-block {
+ margin-left: 0;
+ right: 0;
+ }
}
.multi-file-commit-list-item {
- display: flex;
- padding: 0;
- align-items: center;
- border-radius: $border-radius-default;
-
.multi-file-discard-btn {
display: none;
margin-top: -2px;
margin-left: auto;
- margin-right: $grid-size;
color: $gl-link-color;
-
- &:focus,
- &:hover {
- text-decoration: underline;
- }
}
&:hover {
- background: $white-normal;
-
.multi-file-discard-btn {
display: flex;
}
@@ -584,25 +590,39 @@
}
}
+.multi-file-commit-list-item,
+.ide-file-list .file {
+ display: flex;
+ align-items: center;
+ margin-left: -$grid-size;
+ margin-right: -$grid-size;
+ padding: $grid-size / 2 $grid-size;
+ border-radius: $border-radius-default;
+ text-align: left;
+
+ &:hover,
+ &:focus {
+ background: $white-normal;
+ }
+}
+
.multi-file-commit-list-path {
- padding: $grid-size / 2;
- padding-left: $grid-size;
+ padding: 0;
background: none;
border: 0;
text-align: left;
width: 100%;
- min-width: 0;
+
+ &:hover,
+ &:focus {
+ outline: 0;
+ }
svg {
min-width: 16px;
vertical-align: middle;
display: inline-block;
}
-
- &:hover,
- &:focus {
- outline: 0;
- }
}
.multi-file-commit-list-file-path {
@@ -619,12 +639,18 @@
.multi-file-commit-form {
position: relative;
- padding: $gl-padding;
background-color: $white-light;
- border-top: 1px solid $white-dark;
border-left: 1px solid $white-dark;
transition: all 0.3s ease;
+ > form,
+ > .commit-form-compact {
+ padding: $gl-padding 0;
+ margin-left: $gl-padding;
+ margin-right: $gl-padding;
+ border-top: 1px solid $white-dark;
+ }
+
.btn {
font-size: $gl-font-size;
}
@@ -787,8 +813,9 @@
display: flex;
flex: 1;
flex-direction: column;
- width: 100%;
min-height: 140px;
+ margin-left: $gl-padding;
+ margin-right: $gl-padding;
&.is-first {
border-bottom: 1px solid $white-dark;
@@ -979,9 +1006,8 @@
.ide-tree-header {
display: flex;
align-items: center;
- padding: 10px 0;
- margin-left: 10px;
- margin-right: 10px;
+ margin-bottom: 8px;
+ padding: 12px 0;
border-bottom: 1px solid $white-dark;
.ide-new-btn {
@@ -1012,9 +1038,9 @@
.commit-form-slide-up-enter-active,
.commit-form-slide-up-leave-active {
position: absolute;
- top: 16px;
- left: 16px;
- right: 16px;
+ top: 0;
+ left: 0;
+ right: 0;
transition: all 0.3s ease;
}
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 7d7ff217e5d..09e143c23e8 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -94,7 +94,7 @@ module Boards
def serialize_as_json(resource)
resource.as_json(
- only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
+ only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight],
labels: true,
issue_endpoints: true,
include_full_project_path: board.group_board?,
diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb
new file mode 100644
index 00000000000..6e8aef52b52
--- /dev/null
+++ b/app/controllers/concerns/accepts_pending_invitations.rb
@@ -0,0 +1,15 @@
+module AcceptsPendingInvitations
+ extend ActiveSupport::Concern
+
+ def accept_pending_invitations
+ return unless resource.active_for_authentication?
+
+ clear_stored_location_for_resource if resource.accept_pending_invitations!.any?
+ end
+
+ def clear_stored_location_for_resource
+ session_key = stored_location_key_for(resource)
+
+ session.delete(session_key)
+ end
+end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 6d9c38d9581..7bc46a6ccc0 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -1,4 +1,6 @@
class ConfirmationsController < Devise::ConfirmationsController
+ include AcceptsPendingInvitations
+
def almost_there
flash[:notice] = nil
render layout: "devise_empty"
@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController
end
def after_confirmation_path_for(resource_name, resource)
+ accept_pending_invitations
+
# incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource)
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f0e5d2aa94e..12a6cd11f80 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController
def destroy
@key = current_user.keys.find(params[:id])
- @key.destroy
+ Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format|
format.html { redirect_to profile_keys_url, status: 302 }
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index f7417a6a5aa..6b40fc2fe68 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page])
.per(30)
- @running_count = PipelinesFinder
- .new(project, scope: 'running').execute.count
+ @running_count = limited_pipelines_count(project, 'running')
+ @pending_count = limited_pipelines_count(project, 'pending')
+ @finished_count = limited_pipelines_count(project, 'finished')
+ @pipelines_count = limited_pipelines_count(project)
- @pending_count = PipelinesFinder
- .new(project, scope: 'pending').execute.count
-
- @finished_count = PipelinesFinder
- .new(project, scope: 'finished').execute.count
-
- @pipelines_count = PipelinesFinder
- .new(project).execute.count
-
- @pipelines.map(&:commit) # List commits for batch loading
+ Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
respond_to do |format|
format.html
@@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
- .represent(@pipelines),
+ .represent(@pipelines, disable_coverage: true),
count: {
all: @pipelines_count,
running: @running_count,
@@ -185,4 +178,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def authorize_update_pipeline!
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
+
+ def limited_pipelines_count(project, scope = nil)
+ finder = PipelinesFinder.new(project, scope: scope)
+
+ view_context.limited_counter_with_delimiter(finder.execute)
+ end
end
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index 1ff08cce8cb..d9fecfecc40 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -11,7 +11,14 @@ module Projects
@hook = ProjectHook.new
# Services
- @services = @project.find_or_initialize_services
+ @services = @project.find_or_initialize_services(exceptions: service_exceptions)
+ end
+
+ private
+
+ # Returns a list of services that should be hidden from the list
+ def service_exceptions
+ @project.disabled_services.dup
end
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 1848c806c41..f5a222b3a48 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,5 +1,6 @@
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
+ include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy]
@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
end
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
+ accept_pending_invitations
super
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
- user.confirmed? ? dashboard_projects_path : users_almost_there_path
+ user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(resource)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 61c10c427dd..d9649e30edc 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -184,7 +184,7 @@ module Ci
end
def playable?
- action? && (manual? || complete?)
+ action? && (manual? || retryable?)
end
def action?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 1f49764e7cc..c26f0b6dcdc 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -406,7 +406,18 @@ module Ci
end
def has_warnings?
- builds.latest.failed_but_allowed.any?
+ number_of_warnings.positive?
+ end
+
+ def number_of_warnings
+ BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
+ Build.where(commit_id: pipeline_ids)
+ .latest
+ .failed_but_allowed
+ .group(:commit_id)
+ .count
+ .each { |id, amount| loader.call(id, amount) }
+ end
end
def set_config_source
diff --git a/app/models/commit.rb b/app/models/commit.rb
index b46f9f34689..56d4c86774e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -224,8 +224,34 @@ class Commit
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
+ def lazy_author
+ BatchLoader.for(author_email.downcase).batch do |emails, loader|
+ # A Hash that maps user Emails to the corresponding User objects. The
+ # Emails at this point are the _primary_ Emails of the Users.
+ users_for_emails = User
+ .by_any_email(emails)
+ .each_with_object({}) { |user, hash| hash[user.email] = user }
+
+ users_for_ids = users_for_emails
+ .values
+ .each_with_object({}) { |user, hash| hash[user.id] = user }
+
+ # Some commits may have used an alternative Email address. In this case we
+ # need to query the "emails" table to map those addresses to User objects.
+ Email
+ .where(email: emails - users_for_emails.keys)
+ .pluck(:email, :user_id)
+ .each { |(email, id)| users_for_emails[email] = users_for_ids[id] }
+
+ users_for_emails.each { |email, user| loader.call(email, user) }
+ end
+ end
+
def author
- User.find_by_any_email(author_email.downcase)
+ # We use __sync so that we get the actual objects back (including an actual
+ # nil), instead of a wrapper, as returning a wrapped nil breaks a lot of
+ # code.
+ lazy_author.__sync
end
request_cache(:author) { author_email.downcase }
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 97d89422594..a7d05722287 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
+ include Presentable
self.table_name = 'ci_builds'
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index cefa5c13c5f..db7254c27e0 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
- scope :order_name_asc, -> { reorder(name: :asc) }
- scope :order_name_desc, -> { reorder(name: :desc) }
+ scope :order_name_asc, -> { reorder("lower(name) asc") }
+ scope :order_name_desc, -> { reorder("lower(name) desc") }
end
module ClassMethods
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 73fc5048dcf..1caf47072bc 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -53,6 +53,10 @@ module TimeTrackable
Gitlab::TimeTrackingFormatter.output(time_estimate)
end
+ def time_estimate=(val)
+ val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
+ end
+
private
def touchable?
diff --git a/app/models/project.rb b/app/models/project.rb
index 0975e64e995..35c873830a7 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -997,7 +997,7 @@ class Project < ActiveRecord::Base
available_services_names = Service.available_services_names - exceptions
- available_services_names.map do |service_name|
+ available_services = available_services_names.map do |service_name|
service = find_service(services, service_name)
if service
@@ -1014,6 +1014,14 @@ class Project < ActiveRecord::Base
end
end
end
+
+ available_services.reject do |service|
+ disabled_services.include?(service.to_param)
+ end
+ end
+
+ def disabled_services
+ []
end
def find_or_initialize_service(name)
diff --git a/app/models/user.rb b/app/models/user.rb
index 474fde36c02..8ef3c3ceff0 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -860,6 +860,16 @@ class User < ActiveRecord::Base
confirmed? && !temp_oauth_email?
end
+ def accept_pending_invitations!
+ pending_invitations.select do |member|
+ member.accept_invite!(self)
+ end
+ end
+
+ def pending_invitations
+ Member.where(invite_email: verified_emails).invite
+ end
+
def all_emails
all_emails = []
all_emails << email unless temp_oauth_email?
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 4873d7ce662..e0aaa5cb736 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -1,16 +1,5 @@
module Ci
- class BuildPresenter < Gitlab::View::Presenter::Delegated
- CALLOUT_FAILURE_MESSAGES = {
- unknown_failure: 'There is an unknown failure, please try again',
- script_failure: 'There has been a script failure. Check the job log for more information',
- api_failure: 'There has been an API failure, please try again',
- stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
- runner_system_failure: 'There has been a runner system failure, please try again',
- missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
- }.freeze
-
- presents :build
-
+ class BuildPresenter < CommitStatusPresenter
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
@@ -44,14 +33,6 @@ module Ci
"#{subject.name} - #{detailed_status.status_tooltip}"
end
- def callout_failure_message
- CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
- end
-
- def recoverable?
- failed? && !unrecoverable?
- end
-
private
def tooltip_for_badge
@@ -61,9 +42,5 @@ module Ci
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
-
- def unrecoverable?
- script_failure? || missing_dependency_failure?
- end
end
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
new file mode 100644
index 00000000000..c7f7aa836bd
--- /dev/null
+++ b/app/presenters/commit_status_presenter.rb
@@ -0,0 +1,24 @@
+class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
+ CALLOUT_FAILURE_MESSAGES = {
+ unknown_failure: 'There is an unknown failure, please try again',
+ script_failure: 'There has been a script failure. Check the job log for more information',
+ api_failure: 'There has been an API failure, please try again',
+ stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
+ runner_system_failure: 'There has been a runner system failure, please try again',
+ missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
+ }.freeze
+
+ presents :build
+
+ def callout_failure_message
+ CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
+ end
+
+ def recoverable?
+ failed? && !unrecoverable?
+ end
+
+ def unrecoverable?
+ script_failure? || missing_dependency_failure?
+ end
+end
diff --git a/app/presenters/generic_commit_status_presenter.rb b/app/presenters/generic_commit_status_presenter.rb
new file mode 100644
index 00000000000..da09df29a37
--- /dev/null
+++ b/app/presenters/generic_commit_status_presenter.rb
@@ -0,0 +1,2 @@
+class GenericCommitStatusPresenter < CommitStatusPresenter
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 6457294b285..f782b411b84 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -4,7 +4,11 @@ class PipelineEntity < Grape::Entity
expose :id
expose :user, using: UserEntity
expose :active?, as: :active
- expose :coverage
+
+ # Coverage isn't always necessary (e.g. when displaying project pipelines in
+ # the UI). Instead of creating an entirely different entity we just allow the
+ # disabling of this specific field whenever necessary.
+ expose :coverage, unless: proc { options[:disable_coverage] }
expose :source
expose :created_at, :updated_at
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
index f78791932a7..df8e82f5f60 100644
--- a/app/services/keys/base_service.rb
+++ b/app/services/keys/base_service.rb
@@ -2,7 +2,7 @@ module Keys
class BaseService
attr_accessor :user, :params
- def initialize(user, params)
+ def initialize(user, params = {})
@user, @params = user, params
@ip_address = @params.delete(:ip_address)
end
diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb
new file mode 100644
index 00000000000..785cfa3a1d8
--- /dev/null
+++ b/app/services/keys/destroy_service.rb
@@ -0,0 +1,12 @@
+module Keys
+ class DestroyService < ::Keys::BaseService
+ def execute(key)
+ key.destroy if destroy_possible?(key)
+ end
+
+ # overriden in EE::Keys::DestroyService
+ def destroy_possible?(key)
+ true
+ end
+ end
+end
diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index 6c93dc69bb0..7eb89339a92 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -2,14 +2,14 @@ module Lfs
class UnlockFileService < BaseService
def execute
unless can?(current_user, :push_code, project)
- raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions'
+ raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions')
end
unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex
error(ex.message, 403)
rescue ActiveRecord::RecordNotFound
- error('Lock not found', 404)
+ error(_('Lock not found'), 404)
rescue => ex
error(ex.message, 500)
end
@@ -24,9 +24,9 @@ module Lfs
success(lock: lock, http_status: :ok)
elsif forced
- error('You must have master access to force delete a lock', 403)
+ error(_('You must have master access to force delete a lock'), 403)
else
- error("#{lock.path} is locked by GitLab User #{lock.user_id}", 403)
+ error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
end
end
diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb
index 4963601ea8b..cce0863d611 100644
--- a/app/services/milestones/base_service.rb
+++ b/app/services/milestones/base_service.rb
@@ -5,6 +5,7 @@ module Milestones
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
+ super
end
end
end
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 1c5b4aecabb..2244d16f0a6 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -121,7 +121,7 @@
%tr
%td.shortcut
.key g
- .key e
+ .key v
%td
Go to the project's activity feed
%tr
@@ -175,6 +175,18 @@
%tr
%td.shortcut
.key g
+ .key e
+ %td
+ Go to environments
+ %tr
+ %td.shortcut
+ .key g
+ .key k
+ %td
+ Go to kubernetes
+ %tr
+ %td.shortcut
+ .key g
.key s
%td
Go to snippets
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index c3ea592a6b5..0023ede2be9 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -212,7 +212,7 @@
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
- = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do
+ = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span
= _('Kubernetes')
- if show_cluster_hint
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index b8b75da3f2f..6730172242b 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -4,7 +4,7 @@
by
= link_to member.created_by.name, user_url(member.created_by)
to join the
- = link_to member_source.human_name, member_source.web_url
+ = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token)
#{member_source.model_name.singular} as #{member.human_access}.
%p
diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
new file mode 100644
index 00000000000..a97e8a2b5cc
--- /dev/null
+++ b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint to render markdown text
+merge_request: 18926
+author: "@blackst0ne"
+type: added
diff --git a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
new file mode 100644
index 00000000000..9f07fcdfa0b
--- /dev/null
+++ b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Apply NestingDepth (level 5) (pages/pipelines.scss)
+merge_request: 18830
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/42531-open-invite-404.yml b/changelogs/unreleased/42531-open-invite-404.yml
new file mode 100644
index 00000000000..73729f4a929
--- /dev/null
+++ b/changelogs/unreleased/42531-open-invite-404.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically accepts project/group invite by email after user signup
+merge_request: 17634
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
new file mode 100644
index 00000000000..b9e70bc5679
--- /dev/null
+++ b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unscrollable Markdown preview of WebIDE on Firefox
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml b/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml
new file mode 100644
index 00000000000..2f885c5c927
--- /dev/null
+++ b/changelogs/unreleased/46177-fix-present-on-generic-commit-status.yml
@@ -0,0 +1,5 @@
+---
+title: Allow CommitStatus class to use presentable methods
+merge_request: 18979
+author:
+type: fixed
diff --git a/changelogs/unreleased/46193-fix-big-estimate.yml b/changelogs/unreleased/46193-fix-big-estimate.yml
new file mode 100644
index 00000000000..d0da0c10033
--- /dev/null
+++ b/changelogs/unreleased/46193-fix-big-estimate.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes 500 error on /estimate BIG_VALUE
+merge_request: 18964
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
new file mode 100644
index 00000000000..609968f3230
--- /dev/null
+++ b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
@@ -0,0 +1,5 @@
+---
+title: Adds keyboard shortcut `g e` for Environments on Project pages
+merge_request: 19002
+author:
+type: added
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
new file mode 100644
index 00000000000..48e51b2615e
--- /dev/null
+++ b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
@@ -0,0 +1,5 @@
+---
+title: Adds keyboard shortcut `g k` for Kubernetes on Project pages
+merge_request: 19002
+author:
+type: added
diff --git a/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
new file mode 100644
index 00000000000..9a7cf0d6944
--- /dev/null
+++ b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
@@ -0,0 +1,5 @@
+---
+title: Changes keyboard shortcut of Activity feed to `g v`
+merge_request: 19002
+author:
+type: changed
diff --git a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
new file mode 100644
index 00000000000..f416e35030e
--- /dev/null
+++ b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
@@ -0,0 +1,5 @@
+---
+title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T`
+merge_request: 19002
+author:
+type: removed
diff --git a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
new file mode 100644
index 00000000000..a0d787e570e
--- /dev/null
+++ b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
@@ -0,0 +1,3 @@
+title: Add anchor for incoming email regex
+merge_request: !18917
+type: added
diff --git a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
new file mode 100644
index 00000000000..d77c5b42497
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for variables expression pattern matching syntax
+merge_request: 18902
+author:
+type: added
diff --git a/changelogs/unreleased/fix-assignee-name-wrap.yml b/changelogs/unreleased/fix-assignee-name-wrap.yml
new file mode 100644
index 00000000000..2407288785f
--- /dev/null
+++ b/changelogs/unreleased/fix-assignee-name-wrap.yml
@@ -0,0 +1,5 @@
+---
+title: Wrapping problem on the issues page has been fixed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
new file mode 100644
index 00000000000..c2a788d6ad0
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
@@ -0,0 +1,5 @@
+---
+title: Do not allow to trigger manual actions that were skipped
+merge_request: 18985
+author:
+type: fixed
diff --git a/changelogs/unreleased/memoize-database-version.yml b/changelogs/unreleased/memoize-database-version.yml
new file mode 100644
index 00000000000..575348a53a1
--- /dev/null
+++ b/changelogs/unreleased/memoize-database-version.yml
@@ -0,0 +1,5 @@
+---
+title: Memoize Gitlab::Database.version
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/pipelines-index-performance.yml b/changelogs/unreleased/pipelines-index-performance.yml
new file mode 100644
index 00000000000..928c2ddab72
--- /dev/null
+++ b/changelogs/unreleased/pipelines-index-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of project pipelines pages
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
new file mode 100644
index 00000000000..aabf9a84bfb
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
@@ -0,0 +1,5 @@
+---
+title: Fix api_json.log not always reporting the right HTTP status code
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-move-delete-groups-api-async.yml b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
new file mode 100644
index 00000000000..1b200cac5c5
--- /dev/null
+++ b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
@@ -0,0 +1,5 @@
+---
+title: Move API group deletion to Sidekiq
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
new file mode 100644
index 00000000000..098e4b1d5fa
--- /dev/null
+++ b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
@@ -0,0 +1,5 @@
+---
+title: "Use case in-sensitive ordering by name for dashboard"
+merge_request: 18553
+author: "@vedharish"
+type: fixed
diff --git a/changelogs/unreleased/zj-calculate-checksum-mandator.yml b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
new file mode 100644
index 00000000000..83315a3c5dd
--- /dev/null
+++ b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
@@ -0,0 +1,5 @@
+---
+title: Remove shellout implementation for Repository checksums
+merge_request:
+author:
+type: other
diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb
index c8d7f742bb1..14616e726d9 100644
--- a/config/initializers/deprecations.rb
+++ b/config/initializers/deprecations.rb
@@ -1,4 +1,4 @@
-if Gitlab.dev_env_or_com?
+if Rails.env.development? || ENV['GITLAB_LEGACY_PATH_LOG_MESSAGE']
deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
deprecator.behavior = -> (message, callstack) {
diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb
new file mode 100644
index 00000000000..d6f25d3d4ab
--- /dev/null
+++ b/db/migrate/20180504195842_project_name_lower_index.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ProjectNameLowerIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+ INDEX_NAME = 'index_projects_on_lower_name'
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+
+ execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))"
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+
+ if supports_drop_index_concurrently?
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
+ else
+ execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
+ end
+ end
+end
diff --git a/doc/api/README.md b/doc/api/README.md
index e777fc63d2b..194907accc7 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -33,6 +33,7 @@ following locations:
- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
+- [Markdown](markdown.md)
- [Merge Requests](merge_requests.md)
- [Project milestones](milestones.md)
- [Group milestones](group_milestones.md)
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 923fd662a5b..96842ef330f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -487,6 +487,9 @@ Parameters:
- `id` (required) - The ID or path of a user group
+This will queue a background job to delete all projects in the group. The
+response will be a 202 Accepted if the user has authorization.
+
## Search for group
Get all groups that match your string in their name or path.
diff --git a/doc/api/markdown.md b/doc/api/markdown.md
new file mode 100644
index 00000000000..f406838e887
--- /dev/null
+++ b/doc/api/markdown.md
@@ -0,0 +1,29 @@
+# Markdown API
+
+> [Introduced][ce-18926] in GitLab 11.0.
+
+Available only in APIv4.
+
+## Render an arbitrary Markdown document
+
+```
+POST /api/v4/markdown
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | ------------- | ------------------------------------------ |
+| `text` | string | yes | The markdown text to render |
+| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` |
+| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. |
+
+```bash
+curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown
+```
+
+Response example:
+
+```json
+{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" }
+```
+
+[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 42367bf13f7..f66b2b374ba 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -530,6 +530,16 @@ Below you can find supported syntax reference:
`$STAGING` value needs to a string, with length higher than zero.
Variable that contains only whitespace characters is not an empty variable.
+1. Pattern matching _(added in 11.0)_
+
+ > Example: `$VARIABLE =~ /^content.*/`
+
+ It is possible perform pattern matching against a variable and regular
+ expression. Expression like this evaluates to truth if matches are found.
+
+ Pattern matching is case-sensitive by default. Use `i` flag modifier, like
+ `/pattern/i` to make a pattern case-insensitive.
+
### Unsupported predefined variables
Because GitLab evaluates variables before creating jobs, we do not support a
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 2a17a51d7f8..3e77a6f58b7 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -344,10 +344,11 @@ job:
kubernetes: active
```
-Example of using variables expressions:
+Examples of using variables expressions:
```yaml
deploy:
+ script: cap staging deploy
only:
refs:
- branches
@@ -356,6 +357,16 @@ deploy:
- $STAGING
```
+Another use case is exluding jobs depending on a commit message _(added in 11.0)_:
+
+```yaml
+end-to-end:
+ script: rake test:end-to-end
+ except:
+ variables:
+ - $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/
+```
+
Learn more about variables expressions on [a separate page][variables-expressions].
## `tags`
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 8997a5889dc..858b03c60bf 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -37,12 +37,13 @@ import state from './state';
Vue.use(Vuex);
-export default new Vuex.Store({
+export const createStore = () => new Vuex.Store({
actions,
getters,
mutations,
state,
});
+export default createStore();
```
### `state.js`
@@ -320,10 +321,11 @@ In order to write unit tests for those components, we need to include the store
```javascript
//component_spec.js
import Vue from 'vue';
-import store from './store';
+import { createStore } from './store';
import component from './component.vue'
describe('component', () => {
+ let store;
let vm;
let Component;
@@ -340,6 +342,8 @@ describe('component', () => {
name: 'Foo',
age: '30',
};
+
+ store = createStore();
// populate the store
store.dispatch('addUser', user);
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index f656057e3da..236408762e3 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
you should disable these mechanisms before downgrading and you should provide
alternative authentication methods to your users.
-### Remove Jenkins CI Service entries from the database
+### Remove Service Integration entries from the database
-The `JenkinsService` class is only available on the Enterprise Edition codebase,
+The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase,
so if you downgrade to the Community Edition, you'll come across the following
error:
@@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o
use another column for that information.)
```
+or
+
+```
+Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
+
+ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This
+error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
+column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
+use another column for that information.)
+```
+
All services are created automatically for every project you have, so in order
to avoid getting this error, you need to remove all instances of the
-`JenkinsService` from your database:
+`JenkinsService` and `GithubService` from your database:
**Omnibus Installation**
```
-$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all"
+$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
```
**Source Installation**
```
-$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production
+$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
```
### Secret variables environment scopes
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 785cc32d590..77139c50d07 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from
docker exec -t <container name> gitlab-rake gitlab:backup:create
```
+If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can
+run the backup task on the gitlab application pod using kubectl
+
+```
+kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create
+```
+
Example output:
```
@@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta
version of GitLab, the restore command will abort with an error. Install the
[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again.
+### Restore for Docker image and gitlab-omnibus helm chart
+
+For GitLab installations using docker image or the gitlab-omnibus helm chart on
+a Kubernetes cluster, restore task expects the restore directories to be empty.
+However, with docker and Kubernetes volume mounts, some system level directories
+may be created at the volume roots, like `lost+found` directory found in Linux
+operating systems. These directories are usually owned by `root`, which can
+cause access permission errors since the restore rake task runs as `git` user.
+So, to restore a GitLab installation, users have to confirm the restore target
+directories are empty.
+
+For both these installation types, the backup tarball has to be available in the
+backup location (default location is `/var/opt/gitlab/backups`).
+
+For docker installations, the restore task can be run from host using the
+command
+
+```
+docker exec -it <name of container> gitlab-rake gitlab:backup:restore
+```
+
+Similarly, for gitlab-omnibus helm chart, the restore task can be run on the
+gitlab application pod using kubectl
+
+```
+kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:restore
+```
+
## Alternative backup strategies
If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow.
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 15567715c98..0b16af2953b 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -23,6 +23,10 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
**Fork** button. Soon you should have a project under your namespace with the
necessary files.
+You can also start a new project from a
+[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if
+you want to use a different language.
+
## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 2e1bd6bfe5c..930f2e73683 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
-| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed |
+| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed |
| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
-| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs |
+| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs |
| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
-| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts |
+| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts |
| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards |
| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments |
+| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes |
| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
+| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
| <kbd>t</kbd> | Go to finding file |
| <kbd>i</kbd> | New issue |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 5139e869c71..de20b2b8e67 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -8,14 +8,15 @@ module API
PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
- use GrapeLogging::Middleware::RequestLogger,
- logger: Logger.new(LOG_FILENAME),
- formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
- include: [
- GrapeLogging::Loggers::FilterParameters.new,
- GrapeLogging::Loggers::ClientEnv.new,
- Gitlab::GrapeLogging::Loggers::UserLogger.new
- ]
+ insert_before Grape::Middleware::Error,
+ GrapeLogging::Middleware::RequestLogger,
+ logger: Logger.new(LOG_FILENAME),
+ formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
+ include: [
+ GrapeLogging::Loggers::FilterParameters.new,
+ GrapeLogging::Loggers::ClientEnv.new,
+ Gitlab::GrapeLogging::Loggers::UserLogger.new
+ ]
allow_access_with_scope :api
prefix :api
@@ -139,6 +140,7 @@ module API
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
+ mount ::API::Markdown
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 0d125cd7831..03b6b30a0d8 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -167,8 +167,10 @@ module API
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group|
- ::Groups::DestroyService.new(group, current_user).execute
+ ::Groups::DestroyService.new(group, current_user).async_execute
end
+
+ accepted!
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb
new file mode 100644
index 00000000000..b9ed68aa584
--- /dev/null
+++ b/lib/api/markdown.rb
@@ -0,0 +1,33 @@
+module API
+ class Markdown < Grape::API
+ params do
+ requires :text, type: String, desc: "The markdown text to render"
+ optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown"
+ optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown"
+ end
+ resource :markdown do
+ desc "Render markdown text" do
+ detail "This feature was introduced in GitLab 11.0."
+ end
+ post do
+ # Explicitly set CommonMark as markdown engine to use.
+ # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done.
+ context = { markdown_engine: :common_mark, only_path: false }
+
+ if params[:project]
+ project = Project.find_by_full_path(params[:project])
+
+ not_found!("Project") unless can?(current_user, :read_project, project)
+
+ context[:project] = project
+ else
+ context[:skip_project_check] = true
+ end
+
+ context[:pipeline] = params[:gfm] ? :full : :plain_markdown
+
+ { html: Banzai.render(params[:text], context) }
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 3844fd4810d..4fa7d196e50 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -131,8 +131,9 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
- present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
+ ::Groups::DestroyService.new(group, current_user).async_execute
+
+ accepted!
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index b9d5ecf70ec..2f023f4f242 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -73,7 +73,7 @@ module Banzai
#
# Note that while the key might exist, its value could be nil!
def validate
- needs :project
+ needs :project unless skip_project_check?
end
# Iterates over all <a> and text() nodes in a document.
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 8b2f05fffec..a1f24e8b093 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -42,9 +42,9 @@ module Banzai
end
def self.transform_context(context)
- context.merge(
- only_path: true,
+ context[:only_path] = true unless context.key?(:only_path)
+ context.merge(
# EmojiFilter
asset_host: Gitlab::Application.config.asset_host,
asset_root: Gitlab.config.gitlab.base_url
diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb
new file mode 100644
index 00000000000..f57df7c5637
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression.rb
@@ -0,0 +1,10 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ ExpressionError = Class.new(StandardError)
+ RuntimeError = Class.new(ExpressionError)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
new file mode 100644
index 00000000000..10957598f76
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Matches < Lexeme::Operator
+ PATTERN = /=~/.freeze
+
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def evaluate(variables = {})
+ text = @left.evaluate(variables)
+ regexp = @right.evaluate(variables)
+
+ regexp.scan(text.to_s).any?
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
new file mode 100644
index 00000000000..9b239c29ea4
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ require_dependency 're2'
+
+ class Pattern < Lexeme::Value
+ PATTERN = %r{^/.+/[ismU]*$}.freeze
+
+ def initialize(regexp)
+ @value = regexp
+
+ unless Gitlab::UntrustedRegexp.valid?(@value)
+ raise Lexer::SyntaxError, 'Invalid regular expression!'
+ end
+ end
+
+ def evaluate(variables = {})
+ Gitlab::UntrustedRegexp.fabricate(@value)
+ rescue RegexpError
+ raise Expression::RuntimeError, 'Invalid regular expression!'
+ end
+
+ def self.build(string)
+ new(string)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index e1c68b7c3c2..4cacb1e62c9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -5,15 +5,17 @@ module Gitlab
class Lexer
include ::Gitlab::Utils::StrongMemoize
+ SyntaxError = Class.new(Expression::ExpressionError)
+
LEXEMES = [
Expression::Lexeme::Variable,
Expression::Lexeme::String,
+ Expression::Lexeme::Pattern,
Expression::Lexeme::Null,
- Expression::Lexeme::Equals
+ Expression::Lexeme::Equals,
+ Expression::Lexeme::Matches
].freeze
- SyntaxError = Class.new(Statement::StatementError)
-
MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS)
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index 09a7c98464b..b36f1e0f865 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -3,15 +3,16 @@ module Gitlab
module Pipeline
module Expression
class Statement
- StatementError = Class.new(StandardError)
+ StatementError = Class.new(Expression::ExpressionError)
GRAMMAR = [
+ %w[variable],
%w[variable equals string],
%w[variable equals variable],
%w[variable equals null],
%w[string equals variable],
%w[null equals variable],
- %w[variable]
+ %w[variable matches pattern]
].freeze
def initialize(statement, variables = {})
@@ -35,11 +36,13 @@ module Gitlab
def truthful?
evaluate.present?
+ rescue Expression::ExpressionError
+ false
end
def valid?
parse_tree.is_a?(Lexeme::Base)
- rescue StatementError
+ rescue Expression::ExpressionError
false
end
end
diff --git a/lib/gitlab/ci/pipeline/preloader.rb b/lib/gitlab/ci/pipeline/preloader.rb
new file mode 100644
index 00000000000..e7a2e5511cf
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/preloader.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ # Class for preloading data associated with pipelines such as commit
+ # authors.
+ module Preloader
+ def self.preload(pipelines)
+ # This ensures that all the pipeline commits are eager loaded before we
+ # start using them.
+ pipelines.each(&:commit)
+
+ pipelines.each do |pipeline|
+ # This preloads the author of every commit. We're using "lazy_author"
+ # here since "author" immediately loads the data on the first call.
+ pipeline.commit.try(:lazy_author)
+
+ # This preloads the number of warnings for every pipeline, ensuring
+ # that Ci::Pipeline#has_warnings? doesn't execute any additional
+ # queries.
+ pipeline.number_of_warnings
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 76501dd50e8..d49d055c3f2 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -43,7 +43,7 @@ module Gitlab
end
def self.version
- database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
+ @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
def self.join_lateral_supported?
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 061865a7acf..b521d69930a 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1572,14 +1572,12 @@ module Gitlab
end
def checksum
- gitaly_migrate(:calculate_checksum,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_repository_client.calculate_checksum
- else
- calculate_checksum_by_shelling_out
- end
- end
+ # The exists? RPC is much cheaper, so we perform this request first
+ raise NoRepository, "Repository does not exists" unless exists?
+
+ gitaly_repository_client.calculate_checksum
+ rescue GRPC::NotFound
+ raise NoRepository # Guard against data races.
end
private
@@ -2478,36 +2476,6 @@ module Gitlab
rev_parse_target(ref).oid
end
- def calculate_checksum_by_shelling_out
- raise NoRepository unless exists?
-
- args = %W(--git-dir=#{path} show-ref --heads --tags)
- output, status = run_git(args)
-
- if status.nil? || !status.zero?
- # Non-valid git repositories return 128 as the status code and an error output
- raise InvalidRepository if status == 128 && output.to_s.downcase =~ /not a git repository/
- # Empty repositories returns with a non-zero status and an empty output.
- raise ChecksumError, output unless output.blank?
-
- return EMPTY_REPOSITORY_CHECKSUM
- end
-
- refs = output.split("\n")
-
- result = refs.inject(nil) do |checksum, ref|
- value = Digest::SHA1.hexdigest(ref).hex
-
- if checksum.nil?
- value
- else
- checksum ^ value
- end
- end
-
- result.to_s(16)
- end
-
def build_git_cmd(*args)
object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index c9122a23568..d323cb9dadf 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -57,7 +57,7 @@ module Gitlab
regex = Regexp.escape(wildcard_address)
regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
- Regexp.new(regex).freeze
+ Regexp.new(/\A#{regex}\z/).freeze
end
end
end
diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb
index 75ba0799058..dc2d91dfa23 100644
--- a/lib/gitlab/untrusted_regexp.rb
+++ b/lib/gitlab/untrusted_regexp.rb
@@ -9,7 +9,9 @@ module Gitlab
# there is a strict limit on total execution time. See the RE2 documentation
# at https://github.com/google/re2/wiki/Syntax for more details.
class UntrustedRegexp
- delegate :===, to: :regexp
+ require_dependency 're2'
+
+ delegate :===, :source, to: :regexp
def initialize(pattern, multiline: false)
if multiline
@@ -35,6 +37,10 @@ module Gitlab
RE2.Replace(text, regexp, rewrite)
end
+ def ==(other)
+ self.source == other.source
+ end
+
# Handles regular expressions with the preferred RE2 library where possible
# via UntustedRegex. Falls back to Ruby's built-in regular expression library
# when the syntax would be invalid in RE2.
@@ -48,6 +54,24 @@ module Gitlab
Regexp.new(pattern)
end
+ def self.valid?(pattern)
+ !!self.fabricate(pattern)
+ rescue RegexpError
+ false
+ end
+
+ def self.fabricate(pattern)
+ matches = pattern.match(%r{^/(?<regexp>.+)/(?<flags>[ismU]*)$})
+
+ raise RegexpError, 'Invalid regular expression!' if matches.nil?
+
+ expression = matches[:regexp]
+ flags = matches[:flags]
+ expression.prepend("(?#{flags})") if flags.present?
+
+ self.new(expression, multiline: false)
+ end
+
private
attr_reader :regexp
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index af30ecb0e9b..e7aab50e42a 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -8,6 +8,7 @@ task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb')
+ require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb')
require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb')
NamespacesProjectsPathLowerIndexes.new.up
@@ -18,5 +19,6 @@ task setup_postgresql: :environment do
IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up
UsersNameLowerIndex.new.up
+ ProjectNameLowerIndex.new.up
AddPathIndexToRedirectRoutes.new.up
end
diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb
index fbf9a4d17e5..0931e649e24 100644
--- a/qa/qa/specs/features/merge_request/create_spec.rb
+++ b/qa/qa/specs/features/merge_request/create_spec.rb
@@ -11,7 +11,7 @@ module QA
expect(page).to have_content('This is a merge request')
expect(page).to have_content('Great feature')
- expect(page).to have_content('Opened less than a minute ago')
+ expect(page).to have_content(/Opened [\w\s]+ a minute ago/)
end
end
end
diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh
index 286b1325f1d..35f68c581f3 100644
--- a/scripts/create_mysql_user.sh
+++ b/scripts/create_mysql_user.sh
@@ -1,7 +1,6 @@
#!/bin/bash
mysql --user=root --host=mysql <<EOF
-CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh
index 8a744df3226..8a049bcd7fb 100644
--- a/scripts/create_postgres_user.sh
+++ b/scripts/create_postgres_user.sh
@@ -1,8 +1,6 @@
#!/bin/bash
psql -h postgres -U postgres postgres <<EOF
-DROP DATABASE IF EXISTS gitlabhq_test;
-CREATE DATABASE gitlabhq_test;
CREATE USER gitlab;
-GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab;
EOF
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 206d62dbc78..d8bcc9f8191 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml
-# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
-# user but not necessarily a full schema loaded
-if [ "$CREATE_DB_USER" != "false" ]; then
- if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
- . scripts/create_postgres_user.sh
- else
- . scripts/create_mysql_user.sh
- fi
-fi
-
if [ "$SETUP_DB" != "false" ]; then
- bundle exec rake db:drop db:create db:schema:load db:migrate
-
- if [ "$GITLAB_DATABASE" = "mysql" ]; then
- bundle exec rake add_limits_mysql
- fi
+ setup_db
+elif getent hosts postgres || getent hosts mysql; then
+ setup_db_user_only
fi
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 6faa701f0ce..2d2ba115563 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -12,3 +12,21 @@ retry() {
done
return 1
}
+
+setup_db_user_only() {
+ if [ "$GITLAB_DATABASE" = "postgresql" ]; then
+ . scripts/create_postgres_user.sh
+ else
+ . scripts/create_mysql_user.sh
+ fi
+}
+
+setup_db() {
+ setup_db_user_only
+
+ bundle exec rake db:drop db:create db:schema:load db:migrate
+
+ if [ "$GITLAB_DATABASE" = "mysql" ]; then
+ bundle exec rake add_limits_mysql
+ fi
+}
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index a451bbb97b6..9e7bc20a6d1 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -35,10 +35,16 @@ describe Projects::PipelinesController do
expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 4
- expect(json_response['count']['all']).to eq 4
- expect(json_response['count']['running']).to eq 1
- expect(json_response['count']['pending']).to eq 1
- expect(json_response['count']['finished']).to eq 1
+ expect(json_response['count']['all']).to eq '4'
+ expect(json_response['count']['running']).to eq '1'
+ expect(json_response['count']['pending']).to eq '1'
+ expect(json_response['count']['finished']).to eq '1'
+ end
+
+ it 'does not include coverage data for the pipelines' do
+ subject
+
+ expect(json_response['pipelines'][0]).not_to include('coverage')
end
context 'when performing gitaly calls', :request_store do
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index e4be6193b8b..a986ddc4abc 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -5,18 +5,41 @@ describe 'Invites' do
let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) }
- let(:invite) { group.group_members.invite.last }
+ let(:group_invite) { group.group_members.invite.last }
before do
project.add_master(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
- invite.generate_invite_token!
+ group_invite.generate_invite_token!
+ end
+
+ def confirm_email_and_sign_in(new_user)
+ new_user_token = User.find_by_email(new_user.email).confirmation_token
+
+ visit user_confirmation_path(confirmation_token: new_user_token)
+ fill_in_sign_in_form(new_user)
+ end
+
+ def fill_in_sign_up_form(new_user)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ fill_in 'new_user_password', with: new_user.password
+ click_button "Register"
+ end
+
+ def fill_in_sign_in_form(user)
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: user.password
+ check 'user_remember_me'
+ click_button 'Sign in'
end
context 'when signed out' do
before do
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'renders sign in page with sign in notice' do
@@ -25,12 +48,9 @@ describe 'Invites' do
end
it 'sign in and redirects to invitation page' do
- fill_in 'user_login', with: user.email
- fill_in 'user_password', with: user.password
- check 'user_remember_me'
- click_button 'Sign in'
+ fill_in_sign_in_form(user)
- expect(current_path).to eq(invite_path(invite.raw_invite_token))
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content(
'You have been invited by John Doe to join group Owned as Developer.'
)
@@ -45,7 +65,7 @@ describe 'Invites' do
end
it 'shows message user already a member' do
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
expect(page).to have_content('However, you are already a member of this group.')
end
end
@@ -53,7 +73,7 @@ describe 'Invites' do
describe 'accepting the invitation' do
before do
sign_in(user)
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
@@ -69,7 +89,7 @@ describe 'Invites' do
context 'when signed in' do
before do
sign_in(user)
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
@@ -83,7 +103,7 @@ describe 'Invites' do
context 'when signed out' do
before do
- visit decline_invite_path(invite.raw_invite_token)
+ visit decline_invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
@@ -94,4 +114,72 @@ describe 'Invites' do
end
end
end
+
+ describe 'invite an user using their email address' do
+ let(:new_user) { build_stubbed(:user) }
+ let(:invite_email) { new_user.email }
+ let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) }
+ let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) }
+
+ before do
+ stub_application_setting(send_user_confirmation_email: send_email_confirmation)
+ visit invite_path(group_invite.raw_invite_token)
+ end
+
+ context 'email confirmation disabled' do
+ let(:send_email_confirmation) { false }
+
+ it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
+ fill_in_sign_up_form(new_user)
+
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(page).to have_content(project.full_name)
+ visit group_path(group)
+ expect(page).to have_content(group.full_name)
+ end
+
+ context 'the user sign-up using a different email address' do
+ let(:invite_email) { build_stubbed(:user).email }
+
+ it 'signs up and redirects to the invitation page' do
+ fill_in_sign_up_form(new_user)
+
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
+ end
+ end
+ end
+
+ context 'email confirmation enabled' do
+ let(:send_email_confirmation) { true }
+
+ it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
+ fill_in_sign_up_form(new_user)
+ confirm_email_and_sign_in(new_user)
+
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content(project.full_name)
+ visit group_path(group)
+ expect(page).to have_content(group.full_name)
+ end
+
+ it "doesn't accept invitations until the user confirm his email" do
+ fill_in_sign_up_form(new_user)
+ sign_in(owner)
+
+ visit project_project_members_path(project)
+ expect(page).to have_content 'Invited'
+ end
+
+ context 'the user sign-up using a different email address' do
+ let(:invite_email) { build_stubbed(:user).email }
+
+ it 'signs up and redirects to the invitation page' do
+ fill_in_sign_up_form(new_user)
+ confirm_email_and_sign_in(new_user)
+
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 47c5a8161d9..495a010b32c 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do
context 'when navigating to the Project pages' do
it 'redirects to the details page' do
+ visit project_issues_path(project)
+
find('body').native.send_key('g')
find('body').native.send_key('p')
@@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do
it 'redirects to the activity page' do
find('body').native.send_key('g')
- find('body').native.send_key('e')
+ find('body').native.send_key('v')
expect(page).to have_active_navigation('Project')
expect(page).to have_active_sub_navigation('Activity')
@@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do
expect(page).to have_active_sub_navigation('List')
end
+ it 'redirects to the issue board page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('b')
+
+ expect(page).to have_active_navigation('Issues')
+ expect(page).to have_active_sub_navigation('Board')
+ end
+
it 'redirects to the new issue page' do
find('body').native.send_key('i')
expect(page).to have_content(project.title)
+ expect(page).to have_content('New Issue')
end
end
@@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do
end
end
+ context 'when navigating to the CI / CD pages' do
+ it 'redirects to the Jobs page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('j')
+
+ expect(page).to have_active_navigation('CI / CD')
+ expect(page).to have_active_sub_navigation('Jobs')
+ end
+ end
+
+ context 'when navigating to the Operations pages' do
+ it 'redirects to the Environments page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('e')
+
+ expect(page).to have_active_navigation('Operations')
+ expect(page).to have_active_sub_navigation('Environments')
+ end
+
+ it 'redirects to the Kubernetes page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('k')
+
+ expect(page).to have_active_navigation('Operations')
+ expect(page).to have_active_sub_navigation('Kubernetes')
+ end
+ end
+
context 'when navigating to the Snippets pages' do
it 'redirects to the snippets page' do
find('body').native.send_key('g')
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index d646bef96f5..568e679abe9 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -1,13 +1,19 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import actionComponent from '~/pipelines/components/graph/action_component.vue';
-import eventHub from '~/pipelines/event_hub';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => {
let component;
+ let mock;
beforeEach(done => {
const ActionComponent = Vue.extend(actionComponent);
+ mock = new MockAdapter(axios);
+
+ mock.onPost('foo.json').reply(200);
+
component = mountComponent(ActionComponent, {
tooltipText: 'bar',
link: 'foo',
@@ -18,15 +24,10 @@ describe('pipeline graph action component', () => {
});
afterEach(() => {
+ mock.restore();
component.$destroy();
});
- it('should emit an event with the provided link', () => {
- eventHub.$on('postAction', link => {
- expect(link).toEqual('foo');
- });
- });
-
it('should render the provided title as a bootstrap tooltip', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
});
@@ -34,10 +35,12 @@ describe('pipeline graph action component', () => {
it('should update bootstrap tooltip when title changes', done => {
component.tooltipText = 'changed';
- setTimeout(() => {
+ component.$nextTick()
+ .then(() => {
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
- done();
- });
+ })
+ .then(done)
+ .catch(done.fail);
});
it('should render an svg', () => {
@@ -45,44 +48,18 @@ describe('pipeline graph action component', () => {
expect(component.$el.querySelector('svg')).toBeDefined();
});
- it('disables the button when clicked', done => {
- component.$el.click();
+ describe('on click', () => {
+ it('emits `pipelineActionRequestComplete` after a successfull request', done => {
+ spyOn(component, '$emit');
- component.$nextTick(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- done();
- });
- });
-
- it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => {
- component.$el.click();
-
- component
- .$nextTick()
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- component.requestFinishedFor = 'foo';
- })
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => {
- component.$el.click();
+ component.$el.click();
- component
- .$nextTick()
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- component.requestFinishedFor = 'bar';
- })
- .then(() => {
- expect(component.$el.getAttribute('disabled')).toEqual('disabled');
- })
- .then(done)
- .catch(done.fail);
+ component.$nextTick()
+ .then(() => {
+ expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index 75156e7bdfd..16f6db39d6a 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -102,4 +102,31 @@ describe('Pipelines stage component', () => {
});
});
});
+
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
+
+ describe('within pipeline table', () => {
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => {
+ spyOn(eventHub, '$emit');
+
+ component.type = 'PIPELINES_TABLE';
+ component.$el.querySelector('button').click();
+
+ setTimeout(() => {
+ component.$el.querySelector('.js-ci-action').click();
+ component.$nextTick()
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ })
+ .then(done)
+ .catch(done.fail);
+ }, 0);
+ });
+ });
+ });
});
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 08718c382b9..83d39b82068 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do
context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } }
- it 'reports an error about invalid statement' do
+ it 'reports an error about invalid expression' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
+ context 'when using invalid variables expressions regexp' do
+ let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
+
+ it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
new file mode 100644
index 00000000000..49e5af52f4d
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -0,0 +1,80 @@
+require 'fast_spec_helper'
+require_dependency 're2'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('=~', left, right))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns false when left and right do not match' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('something'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'returns true when left and right match' do
+ allow(left).to receive(:evaluate).and_return('my-awesome-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'supports matching against a nil value' do
+ allow(left).to receive(:evaluate).and_return(nil)
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('pattern'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'supports multiline strings' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('text-string'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'supports regexp flags' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My AWESOME content
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
new file mode 100644
index 00000000000..3ebc2e94727
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
@@ -0,0 +1,96 @@
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('/.*/'))
+ .to be_a(described_class)
+ end
+
+ it 'raises an error if pattern is invalid' do
+ expect { described_class.build('/ some ( thin/i') }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError)
+ end
+ end
+
+ describe '.type' do
+ it 'is a value lexeme' do
+ expect(described_class.type).to eq :value
+ end
+ end
+
+ describe '.scan' do
+ it 'correctly identifies a pattern token' do
+ scanner = StringScanner.new('/pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('pattern')
+ end
+
+ it 'is a greedy scanner for regexp boundaries' do
+ scanner = StringScanner.new('/some .* / pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
+ end
+
+ it 'does not allow to use an empty pattern' do
+ scanner = StringScanner.new(%(//))
+
+ token = described_class.scan(scanner)
+
+ expect(token).to be_nil
+ end
+
+ it 'support single flag' do
+ scanner = StringScanner.new('/pattern/i')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('(?i)pattern')
+ end
+
+ it 'support multiple flags' do
+ scanner = StringScanner.new('/pattern/im')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('(?im)pattern')
+ end
+
+ it 'does not support arbitrary flags' do
+ scanner = StringScanner.new('/pattern/x')
+
+ token = described_class.scan(scanner)
+
+ expect(token).to be_nil
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns a regular expression' do
+ regexp = described_class.new('/abc/')
+
+ expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc')
+ end
+
+ it 'raises error if evaluated regexp is not valid' do
+ allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true)
+
+ regexp = described_class.new('/invalid ( .*/')
+
+ expect { regexp.evaluate }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
index 230ceeb07f8..3f11b3f7673 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
end
describe '#tokens' do
- it 'tokenss single value' do
+ it 'returns single value' do
tokens = described_class.new('$VARIABLE').tokens
expect(tokens).to be_one
@@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens).to all(be_an_instance_of(token_class))
end
- it 'tokenss multiple values of the same token' do
+ it 'returns multiple values of the same token' do
tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens
expect(tokens.size).to eq 2
expect(tokens).to all(be_an_instance_of(token_class))
end
- it 'tokenss multiple values with different tokens' do
+ it 'returns multiple values with different tokens' do
tokens = described_class.new('$VARIABLE "text" "value"').tokens
expect(tokens.size).to eq 3
@@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens.third.value).to eq '"value"'
end
- it 'tokenss tokens and operators' do
+ it 'returns tokens and operators' do
tokens = described_class.new('$VARIABLE == "text"').tokens
expect(tokens.size).to eq 3
diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
index e8e6f585310..2b78b1dd4a7 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 6685bf5385b..11e73294f18 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -1,4 +1,5 @@
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
describe Gitlab::Ci::Pipeline::Expression::Statement do
subject do
@@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
'== "123"', # invalid left side
'"some string"', # only string provided
'$VAR ==', # invalid right side
- '12345', # unknown syntax
+ 'null', # missing lexemes
'' # empty statement
]
@@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context "when expression grammar is #{syntax.inspect}" do
let(:text) { syntax }
- it 'aises a statement error exception' do
+ it 'raises a statement error exception' do
expect { subject.parse_tree }
.to raise_error described_class::StatementError
end
@@ -82,48 +83,66 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
describe '#evaluate' do
- statements = [
- ['$PRESENT_VARIABLE == "my variable"', true],
- ["$PRESENT_VARIABLE == 'my variable'", true],
- ['"my variable" == $PRESENT_VARIABLE', true],
- ['$PRESENT_VARIABLE == null', false],
- ['$EMPTY_VARIABLE == null', false],
- ['"" == $EMPTY_VARIABLE', true],
- ['$EMPTY_VARIABLE', ''],
- ['$UNDEFINED_VARIABLE == null', true],
- ['null == $UNDEFINED_VARIABLE', true],
- ['$PRESENT_VARIABLE', 'my variable'],
- ['$UNDEFINED_VARIABLE', nil]
- ]
-
- statements.each do |expression, value|
- context "when using expression `#{expression}`" do
- let(:text) { expression }
-
- it "evaluates to `#{value.inspect}`" do
- expect(subject.evaluate).to eq value
- end
+ using RSpec::Parameterized::TableSyntax
+
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ '"my variable" == $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE == null' | false
+ '$EMPTY_VARIABLE == null' | false
+ '"" == $EMPTY_VARIABLE' | true
+ '$EMPTY_VARIABLE' | ''
+ '$UNDEFINED_VARIABLE == null' | true
+ 'null == $UNDEFINED_VARIABLE' | true
+ '$PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | true
+ "$PRESENT_VARIABLE =~ /^var.*/" | false
+ "$EMPTY_VARIABLE =~ /var.*/" | false
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ "$PRESENT_VARIABLE =~ /VAR.*/i" | true
+ end
+
+ with_them do
+ let(:text) { expression }
+
+ it "evaluates to `#{params[:value].inspect}`" do
+ expect(subject.evaluate).to eq value
end
end
end
describe '#truthful?' do
- statements = [
- ['$PRESENT_VARIABLE == "my variable"', true],
- ["$PRESENT_VARIABLE == 'no match'", false],
- ['$UNDEFINED_VARIABLE == null', true],
- ['$PRESENT_VARIABLE', true],
- ['$UNDEFINED_VARIABLE', false],
- ['$EMPTY_VARIABLE', false]
- ]
-
- statements.each do |expression, value|
- context "when using expression `#{expression}`" do
- let(:text) { expression }
-
- it "returns `#{value.inspect}`" do
- expect(subject.truthful?).to eq value
- end
+ using RSpec::Parameterized::TableSyntax
+
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ "$PRESENT_VARIABLE == 'no match'" | false
+ '$UNDEFINED_VARIABLE == null' | true
+ '$PRESENT_VARIABLE' | true
+ '$UNDEFINED_VARIABLE' | false
+ '$EMPTY_VARIABLE' | false
+ '$INVALID = 1' | false
+ "$PRESENT_VARIABLE =~ /var.*/" | true
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ end
+
+ with_them do
+ let(:text) { expression }
+
+ it "returns `#{params[:value].inspect}`" do
+ expect(subject.truthful?).to eq value
+ end
+ end
+
+ context 'when evaluating expression raises an error' do
+ let(:text) { '$PRESENT_VARIABLE' }
+
+ it 'returns false' do
+ allow(subject).to receive(:evaluate)
+ .and_raise(described_class::StatementError)
+
+ expect(subject.truthful?).to be_falsey
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
index 6d7453f0de5..cedfe270f9d 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Token do
let(:value) { '$VARIABLE' }
diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
new file mode 100644
index 00000000000..477c7477df0
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Preloader do
+ describe '.preload' do
+ it 'preloads the author of every pipeline commit' do
+ commit = double(:commit)
+ pipeline = double(:pipeline, commit: commit)
+
+ expect(commit)
+ .to receive(:lazy_author)
+
+ expect(pipeline)
+ .to receive(:number_of_warnings)
+
+ described_class.preload([pipeline])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 1fe1d3926ad..8ac36ae8bab 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::Database do
end
describe '.version' do
+ around do |example|
+ described_class.instance_variable_set(:@version, nil)
+ example.run
+ described_class.instance_variable_set(:@version, nil)
+ end
+
context "on mysql" do
it "extracts the version number" do
allow(described_class).to receive(:database_version)
@@ -49,6 +55,14 @@ describe Gitlab::Database do
expect(described_class.version).to eq '9.4.4'
end
end
+
+ it 'memoizes the result' do
+ count = ActiveRecord::QueryRecorder
+ .new { 2.times { described_class.version } }
+ .count
+
+ expect(count).to eq(1)
+ end
end
describe '.join_lateral_supported?' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index fcb690d8aa3..2b5710ac401 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2247,66 +2247,42 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#checksum' do
- shared_examples 'calculating checksum' do
- it 'calculates the checksum for non-empty repo' do
- expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
- end
-
- it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
- FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
-
- system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
- chdir: storage_path,
- out: '/dev/null',
- err: '/dev/null')
+ it 'calculates the checksum for non-empty repo' do
+ expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
+ end
- empty_repo = described_class.new('default', 'empty-repo.git', '')
+ it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
+ FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
- expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
- end
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
+ chdir: storage_path,
+ out: '/dev/null',
+ err: '/dev/null')
- it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
- FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
+ empty_repo = described_class.new('default', 'empty-repo.git', '')
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
- chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
+ expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
+ end
- File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
+ it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
+ FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
- non_valid = described_class.new('default', 'non-valid.git', '')
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
- expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
- end
+ File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
- it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
- broken_repo = described_class.new('default', 'a/path.git', '')
+ non_valid = described_class.new('default', 'non-valid.git', '')
- expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
end
- context 'when calculate_checksum Gitaly feature is enabled' do
- it_behaves_like 'calculating checksum'
- end
-
- context 'when calculate_checksum Gitaly feature is disabled', :disable_gitaly do
- it_behaves_like 'calculating checksum'
-
- describe 'when storage is broken', :broken_storage do
- it 'raises a storage exception when storage is not available' do
- broken_repo = described_class.new('broken', 'a/path.git', '')
-
- expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible)
- end
- end
-
- it "raises a Gitlab::Git::Repository::Failure error if the `popen` call to git returns a non-zero exit code" do
- allow(repository).to receive(:popen).and_return(['output', nil])
+ it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
+ broken_repo = described_class.new('default', 'a/path.git', '')
- expect { repository.checksum }.to raise_error Gitlab::Git::Repository::ChecksumError
- end
+ expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index ad087f42e06..4c0c3fcbcc7 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -83,6 +83,10 @@ describe Gitlab::IncomingEmail do
it "returns reply key" do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end
+
+ it 'does not match emails with extra bits' do
+ expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil
+ end
end
context 'self.key_from_fallback_message_id' do
diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb
index 0ee7fa1e570..0a6ac0aa294 100644
--- a/spec/lib/gitlab/untrusted_regexp_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp_spec.rb
@@ -1,6 +1,49 @@
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'support/shared_examples/malicious_regexp_shared_examples'
describe Gitlab::UntrustedRegexp do
+ describe '.valid?' do
+ it 'returns true if regexp is valid' do
+ expect(described_class.valid?('/some ( thing/'))
+ .to be false
+ end
+
+ it 'returns true if regexp is invalid' do
+ expect(described_class.valid?('/some .* thing/'))
+ .to be true
+ end
+ end
+
+ describe '.fabricate' do
+ context 'when regexp is using /regexp/ scheme with flags' do
+ it 'fabricates regexp with a single flag' do
+ regexp = described_class.fabricate('/something/i')
+
+ expect(regexp).to eq described_class.new('(?i)something')
+ expect(regexp.scan('SOMETHING')).to be_one
+ end
+
+ it 'fabricates regexp with multiple flags' do
+ regexp = described_class.fabricate('/something/im')
+
+ expect(regexp).to eq described_class.new('(?im)something')
+ end
+
+ it 'fabricates regexp without flags' do
+ regexp = described_class.fabricate('/something/')
+
+ expect(regexp).to eq described_class.new('something')
+ end
+ end
+
+ context 'when regexp is a raw pattern' do
+ it 'raises an error' do
+ expect { described_class.fabricate('some .* thing') }
+ .to raise_error(RegexpError)
+ end
+ end
+ end
+
describe '#initialize' do
subject { described_class.new(pattern) }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 84ddbbbf2ee..8a52c151cc4 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -594,7 +594,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.full_name} project"
is_expected.to have_html_escaped_body_text project.full_name
- is_expected.to have_body_text project.web_url
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index dc810489011..7d8bddbcedb 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1270,6 +1270,46 @@ describe Ci::Build do
end
end
+ describe '#playable?' do
+ context 'when build is a manual action' do
+ context 'when build has been skipped' do
+ subject { build_stubbed(:ci_build, :manual, status: :skipped) }
+
+ it { is_expected.not_to be_playable }
+ end
+
+ context 'when build has been canceled' do
+ subject { build_stubbed(:ci_build, :manual, status: :canceled) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build is successful' do
+ subject { build_stubbed(:ci_build, :manual, status: :success) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build has failed' do
+ subject { build_stubbed(:ci_build, :manual, status: :failed) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build is a manual untriggered action' do
+ subject { build_stubbed(:ci_build, :manual, status: :manual) }
+
+ it { is_expected.to be_playable }
+ end
+ end
+
+ context 'when build is not a manual action' do
+ subject { build_stubbed(:ci_build, :success) }
+
+ it { is_expected.not_to be_playable }
+ end
+ end
+
describe 'project settings' do
describe '#allow_git_fetch' do
it 'return project allow_git_fetch configuration' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ddd66a6be87..e7845b693a1 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -774,6 +774,33 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#number_of_warnings' do
+ it 'returns the number of warnings' do
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
+
+ expect(pipeline.number_of_warnings).to eq(1)
+ end
+
+ it 'supports eager loading of the number of warnings' do
+ pipeline2 = create(:ci_empty_pipeline, status: :created, project: project)
+
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop')
+
+ pipelines = project.pipelines.to_a
+
+ pipelines.each(&:number_of_warnings)
+
+ # To run the queries we need to actually use the lazy objects, which we do
+ # by just sending "to_i" to them.
+ amount = ActiveRecord::QueryRecorder
+ .new { pipelines.each { |p| p.number_of_warnings.to_i } }
+ .count
+
+ expect(amount).to eq(1)
+ end
+ end
+
shared_context 'with some outdated pipelines' do
before do
create_pipeline(:canceled, 'ref', 'A', project)
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 448b813c2e1..090f91168ad 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -52,22 +52,98 @@ describe Commit do
end
end
- describe '#author' do
+ describe '#author', :request_store do
it 'looks up the author in a case-insensitive way' do
user = create(:user, email: commit.author_email.upcase)
expect(commit.author).to eq(user)
end
- it 'caches the author', :request_store do
+ it 'caches the author' do
user = create(:user, email: commit.author_email)
- expect(User).to receive(:find_by_any_email).and_call_original
expect(commit.author).to eq(user)
+
key = "Commit:author:#{commit.author_email.downcase}"
- expect(RequestStore.store[key]).to eq(user)
+ expect(RequestStore.store[key]).to eq(user)
expect(commit.author).to eq(user)
end
+
+ context 'using eager loading' do
+ let!(:alice) { create(:user, email: 'alice@example.com') }
+ let!(:bob) { create(:user, email: 'hunter2@example.com') }
+
+ let(:alice_commit) do
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'alice@example.com'
+ end
+ end
+
+ let(:bob_commit) do
+ # The commit for Bob uses one of his alternative Emails, instead of the
+ # primary one.
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'bob@example.com'
+ end
+ end
+
+ let(:eve_commit) do
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'eve@example.com'
+ end
+ end
+
+ let!(:commits) { [alice_commit, bob_commit, eve_commit] }
+
+ before do
+ create(:email, user: bob, email: 'bob@example.com')
+ end
+
+ it 'executes only two SQL queries' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ # Running this first ensures we don't run one query for every
+ # commit.
+ commits.each(&:lazy_author)
+
+ # This forces the execution of the SQL queries necessary to load the
+ # data.
+ commits.each { |c| c.author.try(:id) }
+ end
+
+ expect(recorder.count).to eq(2)
+ end
+
+ it "preloads the authors for Commits matching a user's primary Email" do
+ commits.each(&:lazy_author)
+
+ expect(alice_commit.author).to eq(alice)
+ end
+
+ it "preloads the authors for Commits using a User's alternative Email" do
+ commits.each(&:lazy_author)
+
+ expect(bob_commit.author).to eq(bob)
+ end
+
+ it 'sets the author to Nil if an author could not be found for a Commit' do
+ commits.each(&:lazy_author)
+
+ expect(eve_commit.author).to be_nil
+ end
+
+ it 'does not execute SQL queries once the authors are preloaded' do
+ commits.each(&:lazy_author)
+ commits.each { |c| c.author.try(:id) }
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ alice_commit.author
+ bob_commit.author
+ eve_commit.author
+ end
+
+ expect(recorder.count).to be_zero
+ end
+ end
end
describe '#to_reference' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 2ed29052dc1..f3f2bc28d2c 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -565,4 +565,10 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
end
+
+ describe '#present' do
+ subject { commit_status.present }
+
+ it { is_expected.to be_a(CommitStatusPresenter) }
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 3d3092b8ac9..bd6bf5b0712 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -266,6 +266,19 @@ describe Issuable do
end
end
+ describe '#time_estimate=' do
+ it 'coerces the value below Gitlab::Database::MAX_INT_VALUE' do
+ expect { issue.time_estimate = 100 }.to change { issue.time_estimate }.to(100)
+ expect { issue.time_estimate = Gitlab::Database::MAX_INT_VALUE + 100 }.to change { issue.time_estimate }.to(Gitlab::Database::MAX_INT_VALUE)
+ end
+
+ it 'skips coercion for not Integer values' do
+ expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
+ expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError)
+ expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError)
+ end
+ end
+
describe '#to_hook_data' do
let(:builder) { double }
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
new file mode 100644
index 00000000000..b821a84d5e0
--- /dev/null
+++ b/spec/models/concerns/sortable_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe Sortable do
+ describe '.order_by' do
+ let(:relation) { Group.all }
+
+ describe 'ordering by id' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(id: :asc)
+
+ relation.order_by('id_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(id: :desc)
+
+ relation.order_by('id_desc')
+ end
+ end
+
+ describe 'ordering by created day' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(created_at: :asc)
+
+ relation.order_by('created_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(created_at: :desc)
+
+ relation.order_by('created_desc')
+ end
+
+ it 'order by "date"' do
+ expect(relation).to receive(:reorder).with(created_at: :desc)
+
+ relation.order_by('created_date')
+ end
+ end
+
+ describe 'ordering by name' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with("lower(name) asc")
+
+ relation.order_by('name_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with("lower(name) desc")
+
+ relation.order_by('name_desc')
+ end
+ end
+
+ describe 'ordering by Updated Time' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(updated_at: :asc)
+
+ relation.order_by('updated_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(updated_at: :desc)
+
+ relation.order_by('updated_desc')
+ end
+ end
+
+ it 'does not call reorder in case of unrecognized ordering' do
+ expect(relation).not_to receive(:reorder)
+
+ relation.order_by('random_ordering')
+ end
+ end
+
+ describe 'sorting groups' do
+ def ordered_group_names(order)
+ Group.all.order_by(order).map(&:name)
+ end
+
+ let!(:ref_time) { Time.parse('2018-05-01 00:00:00') }
+ let!(:group1) { create(:group, name: 'aa', id: 1, created_at: ref_time - 15.seconds, updated_at: ref_time) }
+ let!(:group2) { create(:group, name: 'AAA', id: 2, created_at: ref_time - 10.seconds, updated_at: ref_time - 5.seconds) }
+ let!(:group3) { create(:group, name: 'BB', id: 3, created_at: ref_time - 5.seconds, updated_at: ref_time - 10.seconds) }
+ let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) }
+
+ it 'sorts groups by id' do
+ expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by name via case-insentitive comparision' do
+ expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by created_at' do
+ expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by updated_at' do
+ expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb))
+ end
+ end
+end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 673049d1cc4..a3e68d2e646 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -78,4 +78,10 @@ describe GenericCommitStatus do
it { is_expected.not_to be_nil }
end
end
+
+ describe '#present' do
+ subject { generic_commit_status.present }
+
+ it { is_expected.to be_a(GenericCommitStatusPresenter) }
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8d3ddd1f87d..684fa030baf 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1223,6 +1223,24 @@ describe User do
end
end
+ describe '#accept_pending_invitations!' do
+ let(:user) { create(:user, email: 'user@email.com') }
+ let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) }
+ let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) }
+ let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') }
+ let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') }
+
+ it 'accepts all the user members pending invitations and returns the accepted_members' do
+ accepted_members = user.accept_pending_invitations!
+
+ expect(accepted_members).to match_array([project_member_invite, group_member_invite])
+ expect(group_member_invite.reload).not_to be_invite
+ expect(project_member_invite.reload).not_to be_invite
+ expect(external_project_member_invite.reload).to be_invite
+ expect(external_group_member_invite.reload).to be_invite
+ end
+ end
+
describe '#all_emails' do
let(:user) { create(:user) }
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index 4bc005df2fc..efd175247b5 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
- expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated)
end
describe '#initialize' do
diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb
new file mode 100644
index 00000000000..f81ee44e371
--- /dev/null
+++ b/spec/presenters/commit_status_presenter_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe CommitStatusPresenter do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ subject(:presenter) do
+ described_class.new(build)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index bb0034e3237..7d923932309 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -738,13 +738,16 @@ describe API::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
- delete api("/groups/#{group1.id}", user1)
+ Sidekiq::Testing.fake! do
+ expect { delete api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(202)
end
it_behaves_like '412 response' do
let(:request) { api("/groups/#{group1.id}", user1) }
+ let(:success_status) { 202 }
end
it "does not remove a group if not an owner" do
@@ -773,7 +776,7 @@ describe API::Groups do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
new file mode 100644
index 00000000000..a55796cf343
--- /dev/null
+++ b/spec/requests/api/markdown_spec.rb
@@ -0,0 +1,112 @@
+require "spec_helper"
+
+describe API::Markdown do
+ RSpec::Matchers.define_negated_matcher :exclude, :include
+
+ describe "POST /markdown" do
+ let(:user) {} # No-op. It gets overwritten in the contexts below.
+
+ before do
+ post api("/markdown", user), params
+ end
+
+ shared_examples "rendered markdown text without GFM" do
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to eq("<p>#{text}</p>")
+ end
+ end
+
+ shared_examples "404 Project Not Found" do
+ it "responses with 404 Not Found" do
+ expect(response).to have_http_status(404)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["message"]).to eq("404 Project Not Found")
+ end
+ end
+
+ context "when arguments are invalid" do
+ context "when text is missing" do
+ let(:params) { {} }
+
+ it "responses with 400 Bad Request" do
+ expect(response).to have_http_status(400)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["error"]).to eq("text is missing")
+ end
+ end
+
+ context "when project is not found" do
+ let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } }
+
+ it_behaves_like "404 Project Not Found"
+ end
+ end
+
+ context "when arguments are valid" do
+ set(:project) { create(:project) }
+ set(:issue) { create(:issue, project: project) }
+ let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" }
+
+ context "when not using gfm" do
+ context "without project" do
+ let(:params) { { text: text } }
+
+ it_behaves_like "rendered markdown text without GFM"
+ end
+
+ context "with project" do
+ let(:params) { { text: text, project: project.full_path } }
+
+ context "when not authorized" do
+ it_behaves_like "404 Project Not Found"
+ end
+
+ context "when authorized" do
+ let(:user) { project.owner }
+
+ it_behaves_like "rendered markdown text without GFM"
+ end
+ end
+ end
+
+ context "when using gfm" do
+ context "without project" do
+ let(:params) { { text: text, gfm: true } }
+
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to include("Hello world!")
+ .and include('data-name="tada"')
+ .and include('data-name="100"')
+ .and include("#1")
+ .and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
+ .and exclude("#1</a>")
+ end
+ end
+
+ context "with project" do
+ let(:params) { { text: text, gfm: true, project: project.full_path } }
+ let(:user) { project.owner }
+
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to include("Hello world!")
+ .and include('data-name="tada"')
+ .and include('data-name="100"')
+ .and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
+ .and include("#1</a>")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index a1cdf583de3..34d4b8e9565 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -458,9 +458,11 @@ describe API::V3::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
- delete v3_api("/groups/#{group1.id}", user1)
+ Sidekiq::Testing.fake! do
+ expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a group if not an owner" do
@@ -489,7 +491,7 @@ describe API::V3::Groups do
it "removes any existing group" do
delete v3_api("/groups/#{group2.id}", admin)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 2473c561f4b..e67d12b7a89 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -26,6 +26,13 @@ describe PipelineEntity do
expect(subject).to include :updated_at, :created_at
end
+ it 'excludes coverage data when disabled' do
+ entity = described_class
+ .represent(pipeline, request: request, disable_coverage: true)
+
+ expect(entity.as_json).not_to include(:coverage)
+ end
+
it 'contains details' do
expect(subject).to include :details
expect(subject[:details])
diff --git a/spec/services/keys/destroy_service_spec.rb b/spec/services/keys/destroy_service_spec.rb
new file mode 100644
index 00000000000..28ac72ddd42
--- /dev/null
+++ b/spec/services/keys/destroy_service_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Keys::DestroyService do
+ let(:user) { create(:user) }
+
+ subject { described_class.new(user) }
+
+ it 'destroys a key' do
+ key = create(:key)
+
+ expect { subject.execute(key) }.to change(Key, :count).by(-1)
+ end
+end
diff --git a/spec/support/shared_examples/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
index ac5d22298bb..65026f1d7c0 100644
--- a/spec/support/shared_examples/malicious_regexp_shared_examples.rb
+++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
@@ -1,3 +1,5 @@
+require 'timeout'
+
shared_examples 'malicious regexp' do
let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' }
let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }