Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js6
-rw-r--r--app/assets/javascripts/jobs/store/utils.js18
-rw-r--r--app/assets/javascripts/pages/groups/registry/repositories/index.js3
-rw-r--r--app/assets/javascripts/registry/components/app.vue117
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue10
-rw-r--r--app/assets/javascripts/registry/components/group_empty_state.vue46
-rw-r--r--app/assets/javascripts/registry/components/project_empty_state.vue133
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue19
-rw-r--r--app/assets/javascripts/registry/index.js31
-rw-r--r--app/assets/javascripts/registry/stores/actions.js2
-rw-r--r--app/assets/javascripts/registry/stores/getters.js1
-rw-r--r--app/assets/javascripts/registry/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js5
-rw-r--r--app/assets/javascripts/registry/stores/state.js1
-rw-r--r--app/controllers/boards/application_controller.rb2
-rw-r--r--app/controllers/boards/lists_controller.rb4
-rw-r--r--app/controllers/concerns/milestone_actions.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/groups/registry/repositories_controller.rb34
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/groups_helper.rb10
-rw-r--r--app/helpers/todos_helper.rb6
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/board.rb5
-rw-r--r--app/models/container_repository.rb1
-rw-r--r--app/models/global_milestone.rb2
-rw-r--r--app/models/gpg_signature.rb2
-rw-r--r--app/models/merge_request_diff.rb18
-rw-r--r--app/models/milestone.rb3
-rw-r--r--app/models/note.rb3
-rw-r--r--app/models/push_event.rb2
-rw-r--r--app/models/todo.rb3
-rw-r--r--app/policies/board_policy.rb4
-rw-r--r--app/policies/milestone_policy.rb2
-rw-r--r--app/serializers/container_repository_entity.rb2
-rw-r--r--app/services/bulk_push_event_payload_service.rb19
-rw-r--r--app/services/event_create_service.rb24
-rw-r--r--app/services/git/base_hooks_service.rb2
-rw-r--r--app/services/git/process_ref_changes_service.rb21
-rw-r--r--app/services/notes/quick_actions_service.rb2
-rw-r--r--app/services/search/snippet_service.rb8
-rw-r--r--app/views/admin/application_settings/_performance.html.haml5
-rw-r--r--app/views/events/event/_push.html.haml12
-rw-r--r--app/views/groups/registry/repositories/index.html.haml12
-rw-r--r--app/views/groups/sidebar/_packages.html.haml16
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/shared/boards/_switcher.html.haml2
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--changelogs/unreleased/17970-preserve-leading-whitespace.yml5
-rw-r--r--changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml5
-rw-r--r--changelogs/unreleased/23315-group-level-container-registry-browser.yml5
-rw-r--r--changelogs/unreleased/2358-elasticsearch-project-snippets.yml5
-rw-r--r--changelogs/unreleased/31007-limit-activity-events.yml5
-rw-r--r--changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml5
-rw-r--r--changelogs/unreleased/id-cleanup-anny-approver-migrations.yml5
-rw-r--r--changelogs/unreleased/id-fix-nplus1-for-signatures.yml5
-rw-r--r--changelogs/unreleased/sh-move-mr-diff-after-commit.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-shell-10-2.yml5
-rw-r--r--config/routes/group.rb2
-rw-r--r--db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb17
-rw-r--r--db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb11
-rw-r--r--db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb44
-rw-r--r--db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb44
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/gitaly/index.md91
-rw-r--r--doc/administration/gitaly/praefect.md10
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md7
-rw-r--r--doc/user/admin_area/settings/img/bulk_push_event_v12_4.pngbin0 -> 28215 bytes
-rw-r--r--doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.pngbin0 -> 46607 bytes
-rw-r--r--doc/user/admin_area/settings/index.md1
-rw-r--r--doc/user/admin_area/settings/push_event_activities_limit.md28
-rw-r--r--doc/user/group/index.md21
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/banzai/reference_parser/mentioned_user_parser.rb18
-rw-r--r--lib/banzai/reference_parser/mentioned_users_by_group_parser.rb33
-rw-r--r--lib/banzai/reference_parser/mentioned_users_by_project_parser.rb19
-rw-r--r--lib/gitlab/ci/config.rb7
-rw-r--r--lib/gitlab/ci/config/edge_stages_injector.rb57
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb2
-rw-r--r--lib/gitlab/data_builder/push.rb8
-rw-r--r--lib/gitlab/gpg/commit.rb14
-rw-r--r--lib/gitlab/metrics/exporter/sidekiq_exporter.rb23
-rw-r--r--lib/gitlab/quick_actions/extractor.rb4
-rw-r--r--lib/gitlab/reference_extractor.rb9
-rw-r--r--locale/gitlab.pot11
-rw-r--r--package.json2
-rw-r--r--scripts/review_apps/base-config.yaml12
-rw-r--r--spec/controllers/groups/registry/repositories_controller_spec.rb90
-rw-r--r--spec/factories/boards.rb8
-rw-r--r--spec/factories/milestones.rb8
-rw-r--r--spec/features/groups/user_sees_package_sidebar_spec.rb47
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb6
-rw-r--r--spec/finders/boards/visits_finder_spec.rb8
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js75
-rw-r--r--spec/frontend/jobs/store/utils_spec.js7
-rw-r--r--spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap61
-rw-r--r--spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap186
-rw-r--r--spec/frontend/registry/components/app_spec.js56
-rw-r--r--spec/frontend/registry/components/collapsible_container_spec.js42
-rw-r--r--spec/frontend/registry/components/group_empty_state_spec.js23
-rw-r--r--spec/frontend/registry/components/project_empty_state_spec.js27
-rw-r--r--spec/frontend/registry/components/table_registry_spec.js77
-rw-r--r--spec/frontend/registry/stores/actions_spec.js18
-rw-r--r--spec/frontend/registry/stores/getters_spec.js6
-rw-r--r--spec/frontend/registry/stores/mutations_spec.js11
-rw-r--r--spec/helpers/groups_helper_spec.rb35
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb51
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb46
-rw-r--r--spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb112
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/stages_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb46
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb3
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb28
-rw-r--r--spec/lib/gitlab/import/merge_request_creator_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb65
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb3
-rw-r--r--spec/models/application_setting_spec.rb4
-rw-r--r--spec/models/gpg_signature_spec.rb12
-rw-r--r--spec/models/note_spec.rb4
-rw-r--r--spec/requests/api/events_spec.rb1
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/serializers/container_repository_entity_spec.rb12
-rw-r--r--spec/services/boards/issues/create_service_spec.rb2
-rw-r--r--spec/services/boards/lists/update_service_spec.rb8
-rw-r--r--spec/services/boards/visits/create_service_spec.rb4
-rw-r--r--spec/services/bulk_push_event_payload_service_spec.rb27
-rw-r--r--spec/services/event_create_service_spec.rb78
-rw-r--r--spec/services/git/base_hooks_service_spec.rb48
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb48
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb12
-rw-r--r--spec/support/api/boards_shared_examples.rb2
-rw-r--r--spec/support/api/milestones_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/boards_create_service.rb2
-rw-r--r--spec/support/shared_examples/services/boards/boards_list_service.rb4
-rw-r--r--spec/views/events/event/_push.html.haml_spec.rb34
-rw-r--r--yarn.lock14
147 files changed, 2384 insertions, 398 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 4149c39eec6..2bd6f7e3927 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-10.1.0
+10.2.0
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 702f00888d0..77c68cac4a6 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -26,7 +26,7 @@ export default {
if (log.append) {
if (isNewJobLogActive()) {
- state.trace = updateIncrementalTrace(log.lines, state.trace);
+ state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
} else {
state.trace += log.html;
}
@@ -35,9 +35,9 @@ export default {
// When the job still does not have a trace
// the trace response will not have a defined
// html or size. We keep the old value otherwise these
- // will be set to `undefined`
+ // will be set to `null`
if (isNewJobLogActive()) {
- state.trace = logLinesParser(log.lines) || state.trace;
+ state.trace = log.lines ? logLinesParser(log.lines) : state.trace;
} else {
state.trace = log.html || state.trace;
}
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index 12069e0c123..58e49f54d96 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -147,13 +147,15 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
const firstNew = newLog[0];
- if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) {
- cloneOldLog.splice(lastIndex);
- } else if (last.lines && last.lines.length) {
- const lastNestedIndex = last.lines.length - 1;
- const lastNested = last.lines[lastNestedIndex];
- if (lastNested.offset === firstNew.offset) {
- last.lines.splice(lastNestedIndex);
+ if (last && firstNew) {
+ if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) {
+ cloneOldLog.splice(lastIndex);
+ } else if (last.lines && last.lines.length) {
+ const lastNestedIndex = last.lines.length - 1;
+ const lastNested = last.lines[lastNestedIndex];
+ if (lastNested.offset === firstNew.offset) {
+ last.lines.splice(lastNestedIndex);
+ }
}
}
@@ -170,7 +172,7 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
* @param array oldLog
* @param array newLog
*/
-export const updateIncrementalTrace = (newLog, oldParsed = []) => {
+export const updateIncrementalTrace = (newLog = [], oldParsed = []) => {
const parsedLog = findOffsetAndRemove(newLog, oldParsed);
return logLinesParser(newLog, parsedLog);
diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js
new file mode 100644
index 00000000000..b663defad0e
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js
@@ -0,0 +1,3 @@
+import initRegistryImages from '~/registry';
+
+document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index a20bae9e37e..11b2c3b7016 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -2,17 +2,19 @@
import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import store from '../stores';
-import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import CollapsibleContainer from './collapsible_container.vue';
+import ProjectEmptyState from './project_empty_state.vue';
+import GroupEmptyState from './group_empty_state.vue';
import { s__, sprintf } from '../../locale';
export default {
name: 'RegistryListApp',
components: {
- clipboardButton,
CollapsibleContainer,
GlEmptyState,
GlLoadingIcon,
+ ProjectEmptyState,
+ GroupEmptyState,
},
props: {
characterError: {
@@ -38,19 +40,27 @@ export default {
},
personalAccessTokensHelpLink: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
registryHostUrlWithPort: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
repositoryUrl: {
type: String,
required: true,
},
+ isGroupPage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
twoFactorAuthHelpLink: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
},
store,
@@ -91,37 +101,10 @@ export default {
false,
);
},
- notLoggedInToRegistryText() {
- return sprintf(
- s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to
- the Container Registry by using your GitLab username and password. If you have
- %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a
- %{personalAccessTokensDocLinkStart}Personal Access Token
- %{personalAccessTokensDocLinkEnd}instead of a password.`),
- {
- twofaDocLinkStart: `<a href="${this.twoFactorAuthHelpLink}" target="_blank">`,
- twofaDocLinkEnd: '</a>',
- personalAccessTokensDocLinkStart: `<a href="${this.personalAccessTokensHelpLink}" target="_blank">`,
- personalAccessTokensDocLinkEnd: '</a>',
- },
- false,
- );
- },
- dockerLoginCommand() {
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- return `docker login ${this.registryHostUrlWithPort}`;
- },
- dockerBuildCommand() {
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- return `docker build -t ${this.repositoryUrl} .`;
- },
- dockerPushCommand() {
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- return `docker push ${this.repositoryUrl}`;
- },
},
created() {
this.setMainEndpoint(this.endpoint);
+ this.setIsDeleteDisabled(this.isGroupPage);
},
mounted() {
if (!this.characterError) {
@@ -129,7 +112,7 @@ export default {
}
},
methods: {
- ...mapActions(['setMainEndpoint', 'fetchRepos']),
+ ...mapActions(['setMainEndpoint', 'fetchRepos', 'setIsDeleteDisabled']),
},
};
</script>
@@ -152,57 +135,19 @@ export default {
<p v-html="introText"></p>
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
</div>
-
- <gl-empty-state
- v-else
- :title="s__('ContainerRegistry|There are no container images stored for this project')"
- :svg-path="noContainersImage"
- class="container-message"
- >
- <template #description>
- <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
- <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
- <p class="js-not-logged-in-to-registry-text" v-html="notLoggedInToRegistryText"></p>
- <div class="input-group append-bottom-10">
- <input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerLoginCommand"
- :title="s__('ContainerRegistry|Copy login command')"
- class="input-group-text"
- />
- </span>
- </div>
- <p>
- {{
- s__(
- 'ContainerRegistry|You can add an image to this registry with the following commands:',
- )
- }}
- </p>
-
- <div class="input-group append-bottom-10">
- <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerBuildCommand"
- :title="s__('ContainerRegistry|Copy build command')"
- class="input-group-text"
- />
- </span>
- </div>
-
- <div class="input-group">
- <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerPushCommand"
- :title="s__('ContainerRegistry|Copy push command')"
- class="input-group-text"
- />
- </span>
- </div>
- </template>
- </gl-empty-state>
+ <project-empty-state
+ v-else-if="!isGroupPage"
+ :no-containers-image="noContainersImage"
+ :help-page-path="helpPagePath"
+ :repository-url="repositoryUrl"
+ :two-factor-auth-help-link="twoFactorAuthHelpLink"
+ :personal-access-tokens-help-link="personalAccessTokensHelpLink"
+ :registry-host-url-with-port="registryHostUrlWithPort"
+ />
+ <group-empty-state
+ v-else-if="isGroupPage"
+ :no-containers-image="noContainersImage"
+ :help-page-path="helpPagePath"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 3e31d24088e..ed48331f459 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon, GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
@@ -35,9 +35,13 @@ export default {
};
},
computed: {
+ ...mapGetters(['isDeleteDisabled']),
iconName() {
return this.isOpen ? 'angle-up' : 'angle-right';
},
+ canDeleteRepo() {
+ return this.repo.canDelete && !this.isDeleteDisabled;
+ },
},
methods: {
...mapActions(['fetchRepos', 'fetchList', 'deleteItem']),
@@ -80,7 +84,7 @@ export default {
<div class="controls d-none d-sm-block float-right">
<gl-button
- v-if="repo.canDelete"
+ v-if="canDeleteRepo"
v-gl-tooltip
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove repository')"
@@ -98,7 +102,7 @@ export default {
<gl-loading-icon v-if="repo.isLoading" size="md" class="append-bottom-20" />
<div v-else-if="!repo.isLoading && isOpen" class="container-image-tags">
- <table-registry v-if="repo.list.length" :repo="repo" />
+ <table-registry v-if="repo.list.length" :repo="repo" :can-delete-repo="canDeleteRepo" />
<div v-else class="nothing-here-block">
{{ s__('ContainerRegistry|No tags in Container Registry for this container image.') }}
diff --git a/app/assets/javascripts/registry/components/group_empty_state.vue b/app/assets/javascripts/registry/components/group_empty_state.vue
new file mode 100644
index 00000000000..7885fd2146d
--- /dev/null
+++ b/app/assets/javascripts/registry/components/group_empty_state.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ name: 'GroupEmptyState',
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ noContainersImage: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ noContainerImagesText() {
+ return sprintf(
+ s__(
+ `ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}`,
+ ),
+ {
+ docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
+ docLinkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ :title="s__('ContainerRegistry|There are no container images available in this group')"
+ :svg-path="noContainersImage"
+ class="container-message"
+ >
+ <template #description>
+ <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/registry/components/project_empty_state.vue b/app/assets/javascripts/registry/components/project_empty_state.vue
new file mode 100644
index 00000000000..80ef31004c8
--- /dev/null
+++ b/app/assets/javascripts/registry/components/project_empty_state.vue
@@ -0,0 +1,133 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ name: 'ProjectEmptyState',
+ components: {
+ ClipboardButton,
+ GlEmptyState,
+ },
+ props: {
+ noContainersImage: {
+ type: String,
+ required: true,
+ },
+ repositoryUrl: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ twoFactorAuthHelpLink: {
+ type: String,
+ required: true,
+ },
+ personalAccessTokensHelpLink: {
+ type: String,
+ required: true,
+ },
+ registryHostUrlWithPort: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ dockerBuildCommand() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `docker build -t ${this.repositoryUrl} .`;
+ },
+ dockerPushCommand() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `docker push ${this.repositoryUrl}`;
+ },
+ dockerLoginCommand() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `docker login ${this.registryHostUrlWithPort}`;
+ },
+ noContainerImagesText() {
+ return sprintf(
+ s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
+ store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`),
+ {
+ docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
+ docLinkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ notLoggedInToRegistryText() {
+ return sprintf(
+ s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to
+ the Container Registry by using your GitLab username and password. If you have
+ %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a
+ %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd}
+ instead of a password.`),
+ {
+ twofaDocLinkStart: `<a href="${this.twoFactorAuthHelpLink}" target="_blank">`,
+ twofaDocLinkEnd: '</a>',
+ personalAccessTokensDocLinkStart: `<a href="${this.personalAccessTokensHelpLink}" target="_blank">`,
+ personalAccessTokensDocLinkEnd: '</a>',
+ },
+ false,
+ );
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ :title="s__('ContainerRegistry|There are no container images stored for this project')"
+ :svg-path="noContainersImage"
+ class="container-message"
+ >
+ <template #description>
+ <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
+ <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
+ <p class="js-not-logged-in-to-registry-text" v-html="notLoggedInToRegistryText"></p>
+ <div class="input-group append-bottom-10">
+ <input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerLoginCommand"
+ :title="s__('ContainerRegistry|Copy login command')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ <p></p>
+ <p>
+ {{
+ s__(
+ 'ContainerRegistry|You can add an image to this registry with the following commands:',
+ )
+ }}
+ </p>
+
+ <div class="input-group append-bottom-10">
+ <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerBuildCommand"
+ :title="s__('ContainerRegistry|Copy build command')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+
+ <div class="input-group">
+ <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerPushCommand"
+ :title="s__('ContainerRegistry|Copy push command')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 00acc0eb04a..ac7272c4d29 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import {
GlButton,
GlFormCheckbox,
@@ -35,6 +35,11 @@ export default {
type: Object,
required: true,
},
+ canDeleteRepo: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
data() {
return {
@@ -45,6 +50,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['isDeleteDisabled']),
bulkDeletePath() {
return this.repo.tagsPath ? this.repo.tagsPath.replace('?format=json', '/bulk_destroy') : '';
},
@@ -165,6 +171,9 @@ export default {
}
}
},
+ canDeleteRow(item) {
+ return item && item.canDelete && !this.isDeleteDisabled;
+ },
},
};
</script>
@@ -175,7 +184,7 @@ export default {
<tr>
<th>
<gl-form-checkbox
- v-if="repo.canDelete"
+ v-if="canDeleteRepo"
class="js-select-all-checkbox"
:checked="selectAllChecked"
@change="onSelectAllChange"
@@ -187,7 +196,7 @@ export default {
<th>{{ s__('ContainerRegistry|Last Updated') }}</th>
<th>
<gl-button
- v-if="repo.canDelete"
+ v-if="canDeleteRepo"
v-gl-tooltip
v-gl-modal="modalId"
:disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0"
@@ -208,7 +217,7 @@ export default {
<tr v-for="(item, index) in repo.list" :key="item.tag" class="registry-image-row">
<td class="check">
<gl-form-checkbox
- v-if="item.canDelete"
+ v-if="canDeleteRow(item)"
class="js-select-checkbox"
:checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)"
@change="updateItemsToBeDeleted(index)"
@@ -244,7 +253,7 @@ export default {
<td class="content action-buttons">
<gl-button
- v-if="item.canDelete"
+ v-if="canDeleteRow(item)"
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index 38c3d67042c..18fd360f586 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -13,29 +13,24 @@ export default () =>
data() {
const { dataset } = document.querySelector(this.$options.el);
return {
- characterError: Boolean(dataset.characterError),
- containersErrorImage: dataset.containersErrorImage,
- endpoint: dataset.endpoint,
- helpPagePath: dataset.helpPagePath,
- noContainersImage: dataset.noContainersImage,
- personalAccessTokensHelpLink: dataset.personalAccessTokensHelpLink,
- registryHostUrlWithPort: dataset.registryHostUrlWithPort,
- repositoryUrl: dataset.repositoryUrl,
- twoFactorAuthHelpLink: dataset.twoFactorAuthHelpLink,
+ registryData: {
+ endpoint: dataset.endpoint,
+ characterError: Boolean(dataset.characterError),
+ helpPagePath: dataset.helpPagePath,
+ noContainersImage: dataset.noContainersImage,
+ containersErrorImage: dataset.containersErrorImage,
+ repositoryUrl: dataset.repositoryUrl,
+ isGroupPage: dataset.isGroupPage,
+ personalAccessTokensHelpLink: dataset.personalAccessTokensHelpLink,
+ registryHostUrlWithPort: dataset.registryHostUrlWithPort,
+ twoFactorAuthHelpLink: dataset.twoFactorAuthHelpLink,
+ },
};
},
render(createElement) {
return createElement('registry-app', {
props: {
- characterError: this.characterError,
- containersErrorImage: this.containersErrorImage,
- endpoint: this.endpoint,
- helpPagePath: this.helpPagePath,
- noContainersImage: this.noContainersImage,
- personalAccessTokensHelpLink: this.personalAccessTokensHelpLink,
- registryHostUrlWithPort: this.registryHostUrlWithPort,
- repositoryUrl: this.repositoryUrl,
- twoFactorAuthHelpLink: this.twoFactorAuthHelpLink,
+ ...this.registryData,
},
});
},
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index a2e0130e79e..2121f518a7a 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -20,7 +20,6 @@ export const fetchRepos = ({ commit, state }) => {
export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
-
return axios
.get(repo.tagsPath, { params: { page } })
.then(response => {
@@ -40,6 +39,7 @@ export const multiDeleteItems = (_, { path, items }) =>
axios.delete(path, { params: { ids: items } });
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
+export const setIsDeleteDisabled = ({ commit }, data) => commit(types.SET_IS_DELETE_DISABLED, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
index f4923512578..ac90bde1b2a 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -1,5 +1,6 @@
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
+export const isDeleteDisabled = state => state.isDeleteDisabled;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js
index 2c69bf11807..6740bfede1a 100644
--- a/app/assets/javascripts/registry/stores/mutation_types.js
+++ b/app/assets/javascripts/registry/stores/mutation_types.js
@@ -1,4 +1,5 @@
export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
+export const SET_IS_DELETE_DISABLED = 'SET_IS_DELETE_DISABLED';
export const SET_REPOS_LIST = 'SET_REPOS_LIST';
export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index 8ace6657ad1..ea5925247d1 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -6,6 +6,10 @@ export default {
Object.assign(state, { endpoint });
},
+ [types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) {
+ Object.assign(state, { isDeleteDisabled });
+ },
+
[types.SET_REPOS_LIST](state, list) {
Object.assign(state, {
repos: list.map(el => ({
@@ -17,6 +21,7 @@ export default {
location: el.location,
name: el.path,
tagsPath: el.tags_path,
+ projectId: el.project_id,
})),
});
},
diff --git a/app/assets/javascripts/registry/stores/state.js b/app/assets/javascripts/registry/stores/state.js
index feeac10cbe1..724c64b4994 100644
--- a/app/assets/javascripts/registry/stores/state.js
+++ b/app/assets/javascripts/registry/stores/state.js
@@ -1,6 +1,7 @@
export default () => ({
isLoading: false,
endpoint: '', // initial endpoint to fetch the repos list
+ isDeleteDisabled: false, // controls the delete buttons in the registry
/**
* Each object in `repos` has the following strucure:
* {
diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb
index eab908ba5ed..15ef6698472 100644
--- a/app/controllers/boards/application_controller.rb
+++ b/app/controllers/boards/application_controller.rb
@@ -13,7 +13,7 @@ module Boards
end
def board_parent
- @board_parent ||= board.parent
+ @board_parent ||= board.resource_parent
end
def record_not_found(exception)
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 90e04414d8d..880f7500708 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -9,7 +9,7 @@ module Boards
skip_before_action :authenticate_user!, only: [:index]
def index
- lists = Boards::Lists::ListService.new(board.parent, current_user).execute(board)
+ lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board)
List.preload_preferences_for_user(lists, current_user)
@@ -17,7 +17,7 @@ module Boards
end
def create
- list = Boards::Lists::CreateService.new(board.parent, current_user, create_list_params).execute(board)
+ list = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board)
if list.valid?
render json: serialize_as_json(list)
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 1ead631663e..672d31ec779 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -35,7 +35,7 @@ module MilestoneActions
render json: tabs_json("shared/milestones/_labels_tab", {
labels: milestone_labels.map do |label|
- label.present(issuable_subject: @milestone.parent)
+ label.present(issuable_subject: @milestone.resource_parent)
end
})
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 1eacae06457..1e9d51cf970 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -44,7 +44,7 @@ class Groups::MilestonesController < Groups::ApplicationController
# all projects milestones states at once.
milestones, update_params = get_milestones_for_update
milestones.each do |milestone|
- Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone)
+ Milestones::UpdateService.new(milestone.resource_parent, current_user, update_params).execute(milestone)
end
redirect_to milestone_path
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
new file mode 100644
index 00000000000..39f6963ee0a
--- /dev/null
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+module Groups
+ module Registry
+ class RepositoriesController < Groups::ApplicationController
+ before_action :verify_container_registry_enabled!
+ before_action :authorize_read_container_image!
+
+ def index
+ track_event(:list_repositories)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ @images = group.container_repositories.with_api_entity_associations
+
+ render json: ContainerRepositoriesSerializer
+ .new(current_user: current_user)
+ .represent(@images)
+ end
+ end
+ end
+
+ private
+
+ def verify_container_registry_enabled!
+ render_404 unless Gitlab.config.registry.enabled
+ end
+
+ def authorize_read_container_image!
+ return render_404 unless can?(current_user, :read_container_image, group)
+ end
+ end
+ end
+end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3c70ff3b59f..115d1031a8a 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -290,7 +290,8 @@ module ApplicationSettingsHelper
:snowplow_cookie_domain,
:snowplow_enabled,
:snowplow_site_id,
- :push_event_hooks_limit
+ :push_event_hooks_limit,
+ :push_event_activities_limit
]
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index f05218efe0c..4f31cc67ccc 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -76,10 +76,10 @@ module GitlabRoutingHelper
end
def edit_milestone_path(entity, *args)
- if entity.parent.is_a?(Group)
- edit_group_milestone_path(entity.parent, entity, *args)
+ if entity.resource_parent.is_a?(Group)
+ edit_group_milestone_path(entity.resource_parent, entity, *args)
else
- edit_project_milestone_path(entity.parent, entity, *args)
+ edit_project_milestone_path(entity.resource_parent, entity, *args)
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 9cba87ac444..811467ca03a 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -15,6 +15,16 @@ module GroupsHelper
%w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
+ def group_packages_nav_link_paths
+ %w[
+ groups/container_registries#index
+ ]
+ end
+
+ def group_container_registry_nav?
+ Gitlab.config.registry.enabled && can?(current_user, :read_container_image, @group)
+ end
+
def group_sidebar_links
@group_sidebar_links ||= get_group_sidebar_links
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index a919c068c42..dce0842060d 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -45,8 +45,8 @@ module TodosHelper
end
def todo_parent_path(todo)
- if todo.parent.is_a?(Group)
- link_to todo.parent.name, group_path(todo.parent)
+ if todo.resource_parent.is_a?(Group)
+ link_to todo.resource_parent.name, group_path(todo.resource_parent)
else
link_to_project(todo.project)
end
@@ -64,7 +64,7 @@ module TodosHelper
if todo.for_commit?
project_commit_path(todo.project, todo.target, path_options)
else
- path = [todo.parent, todo.target]
+ path = [todo.resource_parent, todo.target]
path.unshift(:pipelines) if todo.build_failed?
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 0724ee8f39d..a07933d4975 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -217,6 +217,9 @@ class ApplicationSetting < ApplicationRecord
validates :push_event_hooks_limit,
numericality: { greater_than_or_equal_to: 0 }
+ validates :push_event_activities_limit,
+ numericality: { greater_than_or_equal_to: 0 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e9aab4a3d05..b341cf04403 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -83,6 +83,7 @@ module ApplicationSettingImplementation
project_export_enabled: true,
protected_ci_variables: false,
push_event_hooks_limit: 3,
+ push_event_activities_limit: 3,
raw_blob_request_limit: 300,
recaptcha_enabled: false,
login_recaptcha_protection_enabled: false,
diff --git a/app/models/board.rb b/app/models/board.rb
index 31011dc4742..f3f938224a4 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -16,10 +16,9 @@ class Board < ApplicationRecord
!group
end
- def parent
- @parent ||= group || project
+ def resource_parent
+ @resource_parent ||= group || project
end
- alias_method :resource_parent, :parent
def group_board?
group_id.present?
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index e055b66989b..27bb76835c7 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -11,6 +11,7 @@ class ContainerRepository < ApplicationRecord
delegate :client, to: :registry
scope :ordered, -> { order(:name) }
+ scope :with_api_entity_associations, -> { preload(:project) }
# rubocop: disable CodeReuse/ServiceClass
def registry
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 1d553fc8312..7d766e1f25c 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -11,7 +11,7 @@ class GlobalMilestone
delegate :title, :state, :due_date, :start_date, :participants, :project,
:group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
- :milestoneish_id, :parent, to: :milestone
+ :milestoneish_id, :resource_parent, to: :milestone
def to_hash
{
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 46cac1d41bb..0c36e51120f 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -23,6 +23,8 @@ class GpgSignature < ApplicationRecord
validates :project_id, presence: true
validates :gpg_key_primary_keyid, presence: true
+ scope :by_commit_sha, ->(shas) { where(commit_sha: shas) }
+
def self.with_key_and_subkeys(gpg_key)
subkey_ids = gpg_key.subkeys.pluck(:id)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index ca50820a879..fe8ba9765b7 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -136,6 +136,7 @@ class MergeRequestDiff < ApplicationRecord
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
+ after_create_commit :set_as_latest_diff
after_save :update_external_diff_store, if: -> { !importing? && saved_change_to_external_diff? }
@@ -150,10 +151,6 @@ class MergeRequestDiff < ApplicationRecord
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
- MergeRequest
- .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
- .update_all(latest_merge_request_diff_id: self.id)
-
ensure_commit_shas
save_commits
save_diffs
@@ -168,6 +165,12 @@ class MergeRequestDiff < ApplicationRecord
keep_around_commits
end
+ def set_as_latest_diff
+ MergeRequest
+ .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
+ .update_all(latest_merge_request_diff_id: self.id)
+ end
+
def ensure_commit_shas
self.start_commit_sha ||= merge_request.target_branch_sha
self.head_commit_sha ||= merge_request.source_branch_sha
@@ -502,11 +505,6 @@ class MergeRequestDiff < ApplicationRecord
merge_request.closed? && merge_request.metrics.latest_closed_at < EXTERNAL_DIFF_CUTOFF.ago
end
- # We can't rely on `merge_request.latest_merge_request_diff_id` because that
- # may have been changed in `save_git_content` without being reflected in
- # the association's instance. This query is always subject to races, but
- # the worst case is that we *don't* make a diff external when we could. The
- # background worker will make it external at a later date.
def old_version?
latest_id = MergeRequest
.where(id: merge_request_id)
@@ -514,7 +512,7 @@ class MergeRequestDiff < ApplicationRecord
.pluck(:latest_merge_request_diff_id)
.first
- self.id != latest_id
+ latest_id && self.id < latest_id
end
def load_diffs(options)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 916c11a8d03..2fa0cfc9b93 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -257,10 +257,9 @@ class Milestone < ApplicationRecord
title.to_slug.normalize.to_s
end
- def parent
+ def resource_parent
group || project
end
- alias_method :resource_parent, :parent
def group_milestone?
group_id.present?
diff --git a/app/models/note.rb b/app/models/note.rb
index 3e645d79e15..43f349c6fa2 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -483,10 +483,9 @@ class Note < ApplicationRecord
Upload.find_by(model: self, path: paths)
end
- def parent
+ def resource_parent
project
end
- alias_method :resource_parent, :parent
private
diff --git a/app/models/push_event.rb b/app/models/push_event.rb
index 4698df39730..6f7365a2763 100644
--- a/app/models/push_event.rb
+++ b/app/models/push_event.rb
@@ -26,6 +26,8 @@ class PushEvent < Event
delegate :commit_count, to: :push_event_payload
alias_method :commits_count, :commit_count
+ delegate :ref_count, to: :push_event_payload
+
# Returns events of pushes that either pushed to an existing ref or created a
# new one.
def self.created_or_pushed
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 6b71845856a..456115872d1 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -144,10 +144,9 @@ class Todo < ApplicationRecord
end
end
- def parent
+ def resource_parent
project
end
- alias_method :resource_parent, :parent
def unmergeable?
action == UNMERGEABLE
diff --git a/app/policies/board_policy.rb b/app/policies/board_policy.rb
index b8435dad3f1..e2b16249c85 100644
--- a/app/policies/board_policy.rb
+++ b/app/policies/board_policy.rb
@@ -3,7 +3,7 @@
class BoardPolicy < BasePolicy
include FindGroupProjects
- delegate { @subject.parent }
+ delegate { @subject.resource_parent }
condition(:is_group_board) { @subject.group_board? }
condition(:is_project_board) { @subject.project_board? }
@@ -19,7 +19,7 @@ class BoardPolicy < BasePolicy
condition(:reporter_of_group_projects) do
next unless @user
- group_projects_for(user: @user, group: @subject.parent)
+ group_projects_for(user: @user, group: @subject.resource_parent)
.visible_to_user_and_access_level(@user, ::Gitlab::Access::REPORTER)
.exists?
end
diff --git a/app/policies/milestone_policy.rb b/app/policies/milestone_policy.rb
index 2d56eea6a78..9cea8ddd7b3 100644
--- a/app/policies/milestone_policy.rb
+++ b/app/policies/milestone_policy.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class MilestonePolicy < BasePolicy
- delegate { @subject.parent }
+ delegate { @subject.resource_parent }
end
diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb
index cc746698a05..db9cf1c7835 100644
--- a/app/serializers/container_repository_entity.rb
+++ b/app/serializers/container_repository_entity.rb
@@ -18,7 +18,7 @@ class ContainerRepositoryEntity < Grape::Entity
alias_method :repository, :object
def project
- request.project
+ request.respond_to?(:project) ? request.project : object.project
end
def can_destroy?
diff --git a/app/services/bulk_push_event_payload_service.rb b/app/services/bulk_push_event_payload_service.rb
new file mode 100644
index 00000000000..54157bc23f9
--- /dev/null
+++ b/app/services/bulk_push_event_payload_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class BulkPushEventPayloadService
+ def initialize(event, push_data)
+ @event = event
+ @push_data = push_data
+ end
+
+ def execute
+ @event.build_push_event_payload(
+ action: @push_data[:action],
+ commit_count: 0,
+ ref_count: @push_data[:ref_count],
+ ref_type: @push_data[:ref_type]
+ )
+
+ @event.push_event_payload.tap(&:save!)
+ end
+end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 395c5fe09ac..f7282c22a52 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -73,15 +73,27 @@ class EventCreateService
end
def push(project, current_user, push_data)
+ create_push_event(PushEventPayloadService, project, current_user, push_data)
+ end
+
+ def bulk_push(project, current_user, push_data)
+ create_push_event(BulkPushEventPayloadService, project, current_user, push_data)
+ end
+
+ private
+
+ def create_record_event(record, current_user, status)
+ create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
+ end
+
+ def create_push_event(service_class, project, current_user, push_data)
# We're using an explicit transaction here so that any errors that may occur
# when creating push payload data will result in the event creation being
# rolled back as well.
event = Event.transaction do
new_event = create_event(project, current_user, Event::PUSHED)
- PushEventPayloadService
- .new(new_event, push_data)
- .execute
+ service_class.new(new_event, push_data).execute
new_event
end
@@ -92,12 +104,6 @@ class EventCreateService
Users::ActivityService.new(current_user, 'push').execute
end
- private
-
- def create_record_event(record, current_user, status)
- create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
- end
-
def create_event(resource_parent, current_user, status, attributes = {})
attributes.reverse_merge!(
action: status,
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index b1faef58e33..0801fd4d03f 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -48,6 +48,8 @@ module Git
# Push events in the activity feed only show information for the
# last commit.
def create_events
+ return unless params.fetch(:create_push_event, true)
+
EventCreateService.new.push(project, current_user, event_push_data)
end
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
index 62159d4e7e5..3052bed51bc 100644
--- a/app/services/git/process_ref_changes_service.rb
+++ b/app/services/git/process_ref_changes_service.rb
@@ -16,8 +16,8 @@ module Git
def process_changes_by_action(ref_type, changes)
changes_by_action = group_changes_by_action(changes)
- changes_by_action.each do |_, changes|
- process_changes(ref_type, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
+ changes_by_action.each do |action, changes|
+ process_changes(ref_type, action, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
end
end
@@ -38,9 +38,11 @@ module Git
(changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit) || Feature.enabled?(:git_push_execute_all_project_hooks, project)
end
- def process_changes(ref_type, changes, execute_project_hooks:)
+ def process_changes(ref_type, action, changes, execute_project_hooks:)
push_service_class = push_service_class_for(ref_type)
+ create_bulk_push_event = changes.size > Gitlab::CurrentSettings.push_event_activities_limit
+
changes.each do |change|
push_service_class.new(
project,
@@ -48,9 +50,20 @@ module Git
change: change,
push_options: params[:push_options],
create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project),
- execute_project_hooks: execute_project_hooks
+ execute_project_hooks: execute_project_hooks,
+ create_push_event: !create_bulk_push_event
).execute
end
+
+ create_bulk_push_event(ref_type, action, changes) if create_bulk_push_event
+ end
+
+ def create_bulk_push_event(ref_type, action, changes)
+ EventCreateService.new.bulk_push(
+ project,
+ current_user,
+ Gitlab::DataBuilder::Push.build_bulk(action: action, ref_type: ref_type, changes: changes)
+ )
end
def push_service_class_for(ref_type)
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index 076df10bf6f..7e6568b5b25 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -50,7 +50,7 @@ module Notes
return if update_params.empty?
return unless supported?(note)
- self.class.noteable_update_service(note).new(note.parent, current_user, update_params).execute(note.noteable)
+ self.class.noteable_update_service(note).new(note.resource_parent, current_user, update_params).execute(note.noteable)
end
end
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 3c8847d3c18..e686d3bf7c2 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -1,13 +1,7 @@
# frozen_string_literal: true
module Search
- class SnippetService
- attr_accessor :current_user, :params
-
- def initialize(user, params)
- @current_user, @params = user, params.dup
- end
-
+ class SnippetService < Search::GlobalService
def execute
Gitlab::SnippetSearchResults.new(current_user, params[:search])
end
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index 22458223b93..6b02521a0f0 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -25,5 +25,10 @@
= f.number_field :push_event_hooks_limit, class: 'form-control'
.form-text.text-muted
= _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.")
+ .form-group
+ = f.label :push_event_activities_limit, class: 'label-bold'
+ = f.number_field :push_event_activities_limit, class: 'form-control'
+ .form-text.text-muted
+ = _('Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 21c418cb0e4..b9e88f3fc47 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -6,11 +6,13 @@
.event-title.d-flex.flex-wrap
= inline_event_icon(event)
- %span.event-type.d-inline-block.append-right-4.pushed #{event.action_name} #{event.ref_type}
- %span.append-right-4
- - commits_link = project_commits_path(project, event.ref_name)
- - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
- = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
+ - many_refs = event.ref_count.to_i > 1
+ %span.event-type.d-inline-block.append-right-4.pushed= many_refs ? "#{event.action_name} #{event.ref_count} #{event.ref_type.pluralize}" : "#{event.action_name} #{event.ref_type}"
+ - unless many_refs
+ %span.append-right-4
+ - commits_link = project_commits_path(project, event.ref_name)
+ - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
+ = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
new file mode 100644
index 00000000000..e85b0713230
--- /dev/null
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -0,0 +1,12 @@
+- page_title _("Container Registry")
+
+%section
+ .row.registry-placeholder.prepend-bottom-10
+ .col-12
+ #js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json),
+ "help_page_path" => help_page_path('user/packages/container_registry/index'),
+ "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
+ "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
+ "repository_url" => "",
+ is_group_page: true,
+ character_error: @character_error.to_s } }
diff --git a/app/views/groups/sidebar/_packages.html.haml b/app/views/groups/sidebar/_packages.html.haml
new file mode 100644
index 00000000000..16b902a18b9
--- /dev/null
+++ b/app/views/groups/sidebar/_packages.html.haml
@@ -0,0 +1,16 @@
+- if group_container_registry_nav?
+ = nav_link(path: group_packages_nav_link_paths) do
+ = link_to group_container_registries_path(@group), title: _('Container Registry') do
+ .nav-icon-container
+ = sprite_icon('package')
+ %span.nav-item-name
+ = _('Packages')
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_container_registries_path(@group), title: _('Container Registry') do
+ %strong.fly-out-top-item-name
+ = _('Packages')
+ %li.divider.fly-out-top-item
+ = nav_link(controller: 'groups/container_registries') do
+ = link_to group_container_registries_path(@group), title: _('Container Registry') do
+ %span= _('Container Registry')
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 7cc7d1783c4..4930c6cf5f7 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -118,7 +118,7 @@
%strong.fly-out-top-item-name
= _('Kubernetes')
- = render_if_exists 'groups/sidebar/packages' # EE-specific
+ = render_if_exists 'groups/sidebar/packages'
- if group_sidebar_link?(:group_members)
= nav_link(path: 'group_members#index') do
diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml
index 79118630762..09a365a290a 100644
--- a/app/views/shared/boards/_switcher.html.haml
+++ b/app/views/shared/boards/_switcher.html.haml
@@ -1,4 +1,4 @@
-- parent = board.parent
+- parent = board.resource_parent
- milestone_filter_opts = { format: :json }
- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board?
- weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : []
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
index 416b4a34651..ae0e5e45afe 100644
--- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -3,6 +3,6 @@
Add list
.dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- - if can?(current_user, :admin_label, board.parent)
+ - if can?(current_user, :admin_label, board.resource_parent)
= render partial: "shared/issuable/label_page_create", locals: { show_add_list: true, add_list: true, add_list_class: 'd-none' }
= dropdown_loading
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index c9458475aa5..9165147ef2a 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -2,7 +2,7 @@
- board = local_assigns.fetch(:board, nil)
- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics
- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
-- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
+- user_can_admin_list = board && can?(current_user, :admin_list, board.resource_parent)
.issues-filters{ class: ("w-100" if type == :boards_modal) }
.issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal }
diff --git a/changelogs/unreleased/17970-preserve-leading-whitespace.yml b/changelogs/unreleased/17970-preserve-leading-whitespace.yml
new file mode 100644
index 00000000000..84085b80547
--- /dev/null
+++ b/changelogs/unreleased/17970-preserve-leading-whitespace.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent the slash command parser from removing leading whitespace from content that is unrelated to slash commands
+merge_request: 18589
+author: Jared Deckard
+type: fixed
diff --git a/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml b/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml
new file mode 100644
index 00000000000..463d8a0ab98
--- /dev/null
+++ b/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Adds separate parsers for mentions of users, groups, projects in markdown content
+merge_request: 18318
+author:
+type: added
diff --git a/changelogs/unreleased/23315-group-level-container-registry-browser.yml b/changelogs/unreleased/23315-group-level-container-registry-browser.yml
new file mode 100644
index 00000000000..4340c565a88
--- /dev/null
+++ b/changelogs/unreleased/23315-group-level-container-registry-browser.yml
@@ -0,0 +1,5 @@
+---
+title: Group level Container Registry browser
+merge_request: 17615
+author:
+type: added
diff --git a/changelogs/unreleased/2358-elasticsearch-project-snippets.yml b/changelogs/unreleased/2358-elasticsearch-project-snippets.yml
new file mode 100644
index 00000000000..28324c1827d
--- /dev/null
+++ b/changelogs/unreleased/2358-elasticsearch-project-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Support ES searches for project snippets
+merge_request: 18459
+author:
+type: fixed
diff --git a/changelogs/unreleased/31007-limit-activity-events.yml b/changelogs/unreleased/31007-limit-activity-events.yml
new file mode 100644
index 00000000000..d5ad588af33
--- /dev/null
+++ b/changelogs/unreleased/31007-limit-activity-events.yml
@@ -0,0 +1,5 @@
+---
+title: Aggregate push events when there are too many
+merge_request: 18239
+author:
+type: changed
diff --git a/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml b/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml
new file mode 100644
index 00000000000..e909c56983b
--- /dev/null
+++ b/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Add two new predefined stages to pipelines
+merge_request: 18205
+author:
+type: added
diff --git a/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml b/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml
new file mode 100644
index 00000000000..979250d4762
--- /dev/null
+++ b/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup background migrations for any approval rules
+merge_request: 18256
+author:
+type: changed
diff --git a/changelogs/unreleased/id-fix-nplus1-for-signatures.yml b/changelogs/unreleased/id-fix-nplus1-for-signatures.yml
new file mode 100644
index 00000000000..e060c771227
--- /dev/null
+++ b/changelogs/unreleased/id-fix-nplus1-for-signatures.yml
@@ -0,0 +1,5 @@
+---
+title: Remove N+1 for fetching commits signatures
+merge_request: 18389
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-move-mr-diff-after-commit.yml b/changelogs/unreleased/sh-move-mr-diff-after-commit.yml
new file mode 100644
index 00000000000..7eb1edcfe4f
--- /dev/null
+++ b/changelogs/unreleased/sh-move-mr-diff-after-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce idle in transaction time when updating a merge request
+merge_request: 18493
+author:
+type: performance
diff --git a/changelogs/unreleased/update-gitlab-shell-10-2.yml b/changelogs/unreleased/update-gitlab-shell-10-2.yml
new file mode 100644
index 00000000000..cc13c18d633
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-shell-10-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Shell to v10.2.0
+merge_request: 18735
+author:
+type: other
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 1baac9874a2..093cde64c85 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -77,6 +77,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
post :pause
end
end
+
+ resources :container_registries, only: [:index], controller: 'registry/repositories'
end
scope(path: '*id',
diff --git a/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb
new file mode 100644
index 00000000000..84befc95d00
--- /dev/null
+++ b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddPushEventActivitiesLimitToApplicationSettings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :push_event_activities_limit, :integer, default: 3)
+ end
+
+ def down
+ remove_column(:application_settings, :push_event_activities_limit)
+ end
+end
diff --git a/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb b/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb
new file mode 100644
index 00000000000..72621971dbb
--- /dev/null
+++ b/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddRefCountToPushEventPayloads < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :push_event_payloads, :ref_count, :integer
+ end
+end
diff --git a/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb b/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb
new file mode 100644
index 00000000000..e1c0f1d6c0c
--- /dev/null
+++ b/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateRemainingAnyApproverRulesForMergeRequests < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 10_000
+ MIGRATION = 'PopulateAnyApprovalRuleForMergeRequests'
+
+ disable_ddl_transaction!
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+
+ scope :with_approvals_before_merge, -> { where.not(approvals_before_merge: 0) }
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ add_concurrent_index :merge_requests, :id,
+ name: 'tmp_merge_requests_with_approvals_before_merge',
+ where: 'approvals_before_merge != 0'
+
+ Gitlab::BackgroundMigration.steal(MIGRATION)
+
+ PopulateRemainingAnyApproverRulesForMergeRequests::MergeRequest.with_approvals_before_merge.each_batch(of: BATCH_SIZE) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::PopulateAnyApprovalRuleForMergeRequests.new.perform(*range)
+ end
+
+ remove_concurrent_index_by_name(:merge_requests, 'tmp_merge_requests_with_approvals_before_merge')
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb b/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb
new file mode 100644
index 00000000000..fce17ffcf16
--- /dev/null
+++ b/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateRemainingAnyApproverRulesForProjects < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 5_000
+ MIGRATION = 'PopulateAnyApprovalRuleForProjects'
+
+ disable_ddl_transaction!
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+
+ scope :with_approvals_before_merge, -> { where.not(approvals_before_merge: 0) }
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ add_concurrent_index :projects, :id,
+ name: 'tmp_projects_with_approvals_before_merge',
+ where: 'approvals_before_merge != 0'
+
+ Gitlab::BackgroundMigration.steal(MIGRATION)
+
+ PopulateRemainingAnyApproverRulesForProjects::Project.with_approvals_before_merge.each_batch(of: BATCH_SIZE) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::PopulateAnyApprovalRuleForProjects.new.perform(*range)
+ end
+
+ remove_concurrent_index_by_name(:projects, 'tmp_projects_with_approvals_before_merge')
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 50627fc9b06..825b66f6dfd 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -339,6 +339,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
t.integer "throttle_incident_management_notification_per_period", default: 3600
t.integer "push_event_hooks_limit", default: 3, null: false
+ t.integer "push_event_activities_limit", default: 3, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
@@ -3158,6 +3159,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
t.binary "commit_to"
t.text "ref"
t.string "commit_title", limit: 70
+ t.integer "ref_count"
t.index ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true
end
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 5dcdf0e85e9..4152f31c726 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -86,7 +86,8 @@ Below we describe how to configure two Gitaly servers one at
`gitaly1.internal` and the other at `gitaly2.internal`
with secret token `abc123secret`. We assume
your GitLab installation has three repository storages: `default`,
-`storage1` and `storage2`.
+`storage1` and `storage2`. You can use as little as just one server with one
+repository storage if desired.
### 1. Installation
@@ -129,7 +130,7 @@ Configure a token on the instance that runs the GitLab Rails application.
Next, on the Gitaly servers, you need to configure storage paths, enable
the network listener and configure the token.
-NOTE: **Note:** if you want to reduce the risk of downtime when you enable
+NOTE: **Note:** If you want to reduce the risk of downtime when you enable
authentication you can temporarily disable enforcement, see [the
documentation on configuring Gitaly
authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication)
@@ -177,20 +178,19 @@ Check the directory layout on your Gitaly server to be sure.
# Don't forget to copy `/etc/gitlab/gitlab-secrets.json` from web server to Gitaly server.
gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
+ # Authentication token to ensure only authorized servers can communicate with
+ # Gitaly server
+ gitaly['auth_token'] = 'abc123secret'
+
# Make Gitaly accept connections on all network interfaces. You must use
# firewalls to restrict access to this address/port.
+ # Comment out following line if you only want to support TLS connections
gitaly['listen_addr'] = "0.0.0.0:8075"
- gitaly['auth_token'] = 'abc123secret'
-
- # To use TLS for Gitaly you need to add
- gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "path/to/cert.pem"
- gitaly['key_path'] = "path/to/key.pem"
```
1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
- For `gitaly1.internal`:
+ On `gitaly1.internal`:
```
gitaly['storage'] = [
@@ -199,7 +199,7 @@ Check the directory layout on your Gitaly server to be sure.
]
```
- For `gitaly2.internal`:
+ On `gitaly2.internal`:
```
gitaly['storage'] = [
@@ -219,11 +219,6 @@ Check the directory layout on your Gitaly server to be sure.
```toml
listen_addr = '0.0.0.0:8075'
- tls_listen_addr = '0.0.0.0:9999'
-
- [tls]
- certificate_path = /path/to/cert.pem
- key_path = /path/to/key.pem
[auth]
token = 'abc123secret'
@@ -231,7 +226,7 @@ Check the directory layout on your Gitaly server to be sure.
1. Append the following to `/home/git/gitaly/config.toml` for each respective server:
- For `gitaly1.internal`:
+ On `gitaly1.internal`:
```toml
[[storage]]
@@ -241,7 +236,7 @@ Check the directory layout on your Gitaly server to be sure.
name = 'storage1'
```
- For `gitaly2.internal`:
+ On `gitaly2.internal`:
```toml
[[storage]]
@@ -369,11 +364,12 @@ To disable Gitaly on a client node:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22602) in GitLab 11.8.
Gitaly supports TLS encryption. To be able to communicate
-with a Gitaly instance that listens for secure connections you will need to use `tls://` url
+with a Gitaly instance that listens for secure connections you will need to use `tls://` URL
scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
You will need to bring your own certificates as this isn't provided automatically.
-The certificate to be used needs to be installed on all Gitaly nodes and on all
+The certificate to be used needs to be installed on all Gitaly nodes, and the
+certificate (or CA of certificate) on all
client nodes that communicate with it following the procedure described in
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
@@ -395,7 +391,7 @@ To configure Gitaly with TLS:
**For Omnibus GitLab**
-1. On the client nodes, edit `/etc/gitlab/gitlab.rb`:
+1. On the client node(s), edit `/etc/gitlab/gitlab.rb` as follows:
```ruby
git_data_dirs({
@@ -407,20 +403,38 @@ To configure Gitaly with TLS:
gitlab_rails['gitaly_token'] = 'abc123secret'
```
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
-1. On the Gitaly server nodes, edit `/etc/gitlab/gitlab.rb`:
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on client node(s).
+1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
+
+ ```sh
+ sudo mkdir -p /etc/gitlab/ssl
+ sudo chmod 700 /etc/gitlab/ssl
+ sudo cp key.pem cert.pem /etc/gitlab/ssl/
+ ```
+
+1. On the Gitaly server node(s), edit `/etc/gitlab/gitlab.rb` and add:
+
+ <!--
+ updates to following example must also be made at
+ https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab
+ -->
```ruby
gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "path/to/cert.pem"
- gitaly['key_path'] = "path/to/key.pem"
+ gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem"
+ gitaly['key_path'] = "/etc/gitlab/ssl/key.pem"
```
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on Gitaly server node(s).
+1. (Optional) After [verifying that all Gitaly traffic is being served over TLS](#observe-type-of-gitaly-connections),
+ you can improve security by disabling non-TLS connections by commenting out
+ or deleting `gitaly['listen_addr']` in `/etc/gitlab/gitlab.rb`, saving the file,
+ and [reconfiguring GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
+ on Gitaly server node(s).
**For installations from source**
-1. On the client nodes, edit `/home/git/gitlab/config/gitlab.yml`:
+1. On the client node(s), edit `/home/git/gitlab/config/gitlab.yml` as follows:
```yaml
gitlab:
@@ -445,18 +459,33 @@ To configure Gitaly with TLS:
data will be stored in this folder. This will no longer be necessary after
[this issue](https://gitlab.com/gitlab-org/gitaly/issues/1282) is resolved.
-1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source).
-1. On the Gitaly server nodes, edit `/home/git/gitaly/config.toml`:
+1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) on client node(s).
+1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
+
+ ```sh
+ sudo mkdir -p /etc/gitlab/ssl
+ sudo chmod 700 /etc/gitlab/ssl
+ sudo cp key.pem cert.pem /etc/gitlab/ssl/
+ ```
+
+1. On the Gitaly server node(s), edit `/home/git/gitaly/config.toml` and add:
```toml
tls_listen_addr = '0.0.0.0:9999'
[tls]
- certificate_path = '/path/to/cert.pem'
- key_path = '/path/to/key.pem'
+ certificate_path = '/etc/gitlab/ssl/cert.pem'
+ key_path = '/etc/gitlab/ssl/key.pem'
```
-1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source).
+1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) on Gitaly server node(s).
+1. (Optional) After [verifying that all Gitaly traffic is being served over TLS](#observe-type-of-gitaly-connections),
+ you can improve security by disabling non-TLS connections by commenting out
+ or deleting `listen_addr` in `/home/git/gitaly/config.toml`, saving the file,
+ and [restarting GitLab](../restart_gitlab.md#installations-from-source)
+ on Gitaly server node(s).
+
+### Observe type of Gitaly connections
To observe what type of connections are actually being used in a
production environment you can use the following Prometheus query:
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 9e47f7767fe..9038675a28f 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -68,20 +68,26 @@ sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
gitaly['enable'] = false
+# virtual_storage_name must match the same storage name given to praefect in git_data_dirs
+praefect['virtual_storage_name'] = 'praefect'
+praefect['auth_token'] = 'super_secret_abc'
praefect['enable'] = true
praefect['storage_nodes'] = [
{
'storage' => 'praefect-git-1',
'address' => 'tcp://praefect-git-1.internal',
+ 'token' => 'token1',
'primary' => true
},
{
'storage' => 'praefect-git-2',
- 'address' => 'tcp://praefect-git-2.internal'
+ 'address' => 'tcp://praefect-git-2.internal',
+ 'token' => 'token2'
},
{
'storage' => 'praefect-git-3',
- 'address' => 'tcp://praefect-git-3.internal'
+ 'address' => 'tcp://praefect-git-3.internal',
+ 'token' => 'token3'
}
]
```
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 24e6f90e844..2d9e435bbb6 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -290,6 +290,7 @@ are listed in the descriptions of the relevant settings.
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. |
| `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.
| `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value. |
+| `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events will be created. [Bulk push events will be created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. |
| `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable reCAPTCHA. |
| `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for reCAPTCHA. |
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. |
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index e9157951c7b..c60b3323105 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -29,7 +29,12 @@ If you want to disable it for a specific project, you can do so in
## Maximum artifacts size **(CORE ONLY)**
The maximum size of the [job artifacts](../../../administration/job_artifacts.md)
-can be set at the project level, group level, and at the instance level. The value is:
+can be set at:
+
+- The instance level.
+- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/21688), the project and group level.
+
+The value is:
- In *MB* and the default is 100MB per job.
- [Set to 1G](../../gitlab_com/index.md#gitlab-cicd) on GitLab.com.
diff --git a/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png b/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png
new file mode 100644
index 00000000000..38e666e32ac
--- /dev/null
+++ b/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png b/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png
new file mode 100644
index 00000000000..fd3775ac4d7
--- /dev/null
+++ b/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png
Binary files differ
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index ff86620dbb2..4ca91ae5339 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -22,6 +22,7 @@ include:
- [Custom templates repository](instance_template_repository.md) **(PREMIUM)**
- [Protected paths](protected_paths.md) **(CORE ONLY)**
- [Help messages for the `/help` page and the login page](help_page.md)
+- [Push event activities limit and bulk push events](push_event_activities_limit.md)
NOTE: **Note:**
You can change the [first day of the week](../../profile/preferences.md) for the entire GitLab instance
diff --git a/doc/user/admin_area/settings/push_event_activities_limit.md b/doc/user/admin_area/settings/push_event_activities_limit.md
new file mode 100644
index 00000000000..9850de0f4b3
--- /dev/null
+++ b/doc/user/admin_area/settings/push_event_activities_limit.md
@@ -0,0 +1,28 @@
+---
+type: reference
+---
+
+# Push event activities limit and bulk push events
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31007) in GitLab 12.4.
+
+This allows you to set the number of changes (branches or tags) in a single push
+to determine whether individual push events or bulk push event will be created.
+Bulk push events will be created if it surpasses that value.
+
+For example, if 4 branches are pushed and the limit is currently set to 3,
+you'll see the following in the activity feed:
+
+![Bulk push event](img/bulk_push_event_v12_4.png)
+
+With this feature, when a single push includes a lot of changes (e.g. 1,000
+branches), only 1 bulk push event will be created instead of creating 1,000 push
+events. This helps in maintaining good system performance and preventing spam on
+the activity feed.
+
+This setting can be modified in **Admin Area > Settings > Network > Performance Optimization**.
+This can also be configured via the [Application settings API](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)
+as `push_event_activities_limit`. The default value is 3, but it can be greater
+than or equal 0.
+
+![Push event activities limit](img/push_event_activities_limit_v12_4.png)
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 49a8643d82d..c4be08c842b 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -178,9 +178,9 @@ There are two different ways to add a new project to a group:
### Default project-creation level
-> [Introduced][ee-2534] in [GitLab Premium][ee] 10.5.
-> Brought to [GitLab Starter][ee] in 10.7.
-> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
+> - [Introduced][ee-2534] in [GitLab Premium][ee] 10.5.
+> - Brought to [GitLab Starter][ee] in 10.7.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
By default, [Developers and Maintainers](../permissions.md#group-members-permissions) can create projects under a group.
@@ -338,8 +338,7 @@ request to add a new user to a project through API will not be possible.
#### IP access restriction **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1985) in
-[GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1985) in [GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0.
To make sure only people from within your organization can access particular
resources, you have the option to restrict access to groups and their
@@ -351,16 +350,20 @@ Add one or more whitelisted IP subnets using CIDR notation in comma separated fo
coming from a different IP address won't be able to access the restricted
content.
-Restriction currently applies to UI, API access and Git actions via SSH.
+Restriction currently applies to:
+
+- UI.
+- API access.
+- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/32113), Git actions via SSH.
+
To avoid accidental lock-out, admins and group owners are are able to access
the group regardless of the IP restriction.
#### Allowed domain restriction **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7297) in
-[GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
-You can restrict access to groups and their underlying projects by
+You can restrict access to groups by
allowing only users with email addresses in particular domains to be added to the group.
Add email domains you want to whitelist and users with emails from different
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 522f3ed5565..897adef5f58 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -933,8 +933,8 @@ module API
end
class PushEventPayload < Grape::Entity
- expose :commit_count, :action, :ref_type, :commit_from, :commit_to
- expose :ref, :commit_title
+ expose :commit_count, :action, :ref_type, :commit_from, :commit_to, :ref,
+ :commit_title, :ref_count
end
class Event < Grape::Entity
@@ -988,11 +988,11 @@ module API
def todo_target_url(todo)
target_type = todo.target_type.underscore
- target_url = "#{todo.parent.class.to_s.underscore}_#{target_type}_url"
+ target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"
Gitlab::Routing
.url_helpers
- .public_send(target_url, todo.parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend
+ .public_send(target_url, todo.resource_parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend
end
def todo_target_anchor(todo)
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index b7a471f14fe..c90ba0c9b5d 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -102,6 +102,7 @@ module API
optional :project_export_enabled, type: Boolean, desc: 'Enable project export'
optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics'
optional :push_event_hooks_limit, type: Integer, desc: "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
+ optional :push_event_activities_limit, type: Integer, desc: 'Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.'
optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
given recaptcha_enabled: ->(val) { val } do
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
diff --git a/lib/banzai/reference_parser/mentioned_user_parser.rb b/lib/banzai/reference_parser/mentioned_user_parser.rb
new file mode 100644
index 00000000000..4b1bcb3ca09
--- /dev/null
+++ b/lib/banzai/reference_parser/mentioned_user_parser.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Banzai
+ module ReferenceParser
+ class MentionedUserParser < BaseParser
+ self.reference_type = :user
+
+ def references_relation
+ User
+ end
+
+ # any user can be mentioned by username
+ def can_read_reference?(user, ref_attr, node)
+ true
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb b/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb
new file mode 100644
index 00000000000..d4ff6a12cd0
--- /dev/null
+++ b/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Banzai
+ module ReferenceParser
+ class MentionedUsersByGroupParser < BaseParser
+ GROUP_ATTR = 'data-group'
+
+ self.reference_type = :user
+
+ def self.data_attribute
+ @data_attribute ||= GROUP_ATTR
+ end
+
+ def references_relation
+ Group
+ end
+
+ def nodes_visible_to_user(user, nodes)
+ groups = lazy { grouped_objects_for_nodes(nodes, Group, GROUP_ATTR) }
+
+ nodes.select do |node|
+ node.has_attribute?(GROUP_ATTR) && can_read_group_reference?(node, user, groups)
+ end
+ end
+
+ def can_read_group_reference?(node, user, groups)
+ node_group = groups[node]
+
+ node_group && can?(user, :read_group, node_group)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb b/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb
new file mode 100644
index 00000000000..79258d81cc3
--- /dev/null
+++ b/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Banzai
+ module ReferenceParser
+ class MentionedUsersByProjectParser < ProjectParser
+ PROJECT_ATTR = 'data-project'
+
+ self.reference_type = :user
+
+ def self.data_attribute
+ @data_attribute ||= PROJECT_ATTR
+ end
+
+ def references_relation
+ Project
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 342dcb2f784..9c1e6277e95 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -78,8 +78,13 @@ module Gitlab
def build_config(config)
initial_config = Gitlab::Config::Loader::Yaml.new(config).load!
initial_config = Config::External::Processor.new(initial_config, @context).perform
+ initial_config = Config::Extendable.new(initial_config).to_hash
- Config::Extendable.new(initial_config).to_hash
+ if Feature.enabled?(:ci_pre_post_pipeline_stages, @context.project, default_enabled: true)
+ initial_config = Config::EdgeStagesInjector.new(initial_config).to_hash
+ end
+
+ initial_config
end
def build_context(project:, sha:, user:)
diff --git a/lib/gitlab/ci/config/edge_stages_injector.rb b/lib/gitlab/ci/config/edge_stages_injector.rb
new file mode 100644
index 00000000000..64ff9f951e4
--- /dev/null
+++ b/lib/gitlab/ci/config/edge_stages_injector.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ class EdgeStagesInjector
+ PRE_PIPELINE = '.pre'
+ POST_PIPELINE = '.post'
+ EDGES = [PRE_PIPELINE, POST_PIPELINE].freeze
+
+ def self.wrap_stages(stages)
+ stages = stages.to_a - EDGES
+ stages.unshift PRE_PIPELINE
+ stages.push POST_PIPELINE
+
+ stages
+ end
+
+ def initialize(config)
+ @config = config.to_h.deep_dup
+ end
+
+ def to_hash
+ if config.key?(:stages)
+ process(:stages)
+ elsif config.key?(:types)
+ process(:types)
+ else
+ config
+ end
+ end
+
+ private
+
+ attr_reader :config
+
+ delegate :wrap_stages, to: :class
+
+ def process(keyword)
+ stages = extract_stages(keyword)
+ return config if stages.empty?
+
+ stages = wrap_stages(stages)
+ config[keyword] = stages
+ config
+ end
+
+ def extract_stages(keyword)
+ stages = config[keyword]
+ return [] unless stages.is_a?(Array)
+
+ stages
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
index 2d715cbc6bb..7e431f0f8bb 100644
--- a/lib/gitlab/ci/config/entry/stages.rb
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def self.default
- %w[build test deploy]
+ Config::EdgeStagesInjector.wrap_stages %w[build test deploy]
end
end
end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 3460e07fdc5..a83b03f540c 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -107,6 +107,14 @@ module Gitlab
}
end
+ def build_bulk(action:, ref_type:, changes:)
+ {
+ action: action,
+ ref_count: changes.count,
+ ref_type: ref_type
+ }
+ end
+
# This method provides a sample data generated with
# existing project and commits to test webhooks
def build_sample(project, user)
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 4b797a0e397..dc71d0b427a 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -10,6 +10,8 @@ module Gitlab
repo = commit.project.repository.raw_repository
@signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
+
+ lazy_signature
end
def signature_text
@@ -28,18 +30,16 @@ module Gitlab
!!(signature_text && signed_text)
end
- # rubocop: disable CodeReuse/ActiveRecord
def signature
return unless has_signature?
return @signature if @signature
- cached_signature = GpgSignature.find_by(commit_sha: @commit.sha)
+ cached_signature = lazy_signature&.itself
return @signature = cached_signature if cached_signature.present?
@signature = create_cached_signature!
end
- # rubocop: enable CodeReuse/ActiveRecord
def update_signature!(cached_signature)
using_keychain do |gpg_key|
@@ -50,6 +50,14 @@ module Gitlab
private
+ def lazy_signature
+ BatchLoader.for(@commit.sha).batch do |shas, loader|
+ GpgSignature.by_commit_sha(shas).each do |signature|
+ loader.call(signature.commit_sha, signature)
+ end
+ end
+ end
+
def using_keychain
Gitlab::Gpg.using_tmp_keychain do
# first we need to get the fingerprint from the signature to query the gpg
diff --git a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
index 4de95edfc18..5ba7b29734b 100644
--- a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
+++ b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
@@ -14,6 +14,29 @@ module Gitlab
def log_filename
File.join(Rails.root, 'log', 'sidekiq_exporter.log')
end
+
+ private
+
+ # Sidekiq Exporter does not work properly in sidekiq-cluster
+ # mode. It tries to start the service on the same port for
+ # each of the cluster workers, this results in failure
+ # due to duplicate binding.
+ #
+ # For now we ignore this error, as metrics are still "kind of"
+ # valid as they are rendered from shared directory.
+ #
+ # Issue: https://gitlab.com/gitlab-org/gitlab/issues/5714
+ def start_working
+ super
+ rescue Errno::EADDRINUSE => e
+ Sidekiq.logger.error(
+ class: self.class.to_s,
+ message: 'Cannot start sidekiq_exporter',
+ exception: e.message
+ )
+
+ false
+ end
end
end
end
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index ff9bb293b47..e04d6f250b1 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -50,7 +50,7 @@ module Gitlab
content, commands = perform_substitutions(content, commands)
- [content.strip, commands]
+ [content.rstrip, commands]
end
private
@@ -109,7 +109,7 @@ module Gitlab
[ ]
(?<arg>[^\n]*)
)?
- (?:\n|$)
+ (?:\s*\n|$)
)
}mix
end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 00f817c2399..ea2b03b42c1 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -3,7 +3,8 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
- REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user epic).freeze
+ REFERABLES = %i(user issue label milestone
+ merge_request snippet commit commit_range directly_addressed_user epic).freeze
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil)
@@ -54,9 +55,9 @@ module Gitlab
def self.references_pattern
return @pattern if @pattern
- patterns = REFERABLES.map do |ref|
- ref.to_s.classify.constantize.try(:reference_pattern)
- end
+ patterns = REFERABLES.map do |type|
+ Banzai::ReferenceParser[type].reference_type.to_s.classify.constantize.try(:reference_pattern)
+ end.uniq
@pattern = Regexp.union(patterns.compact)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index dae5327c398..e17d7c29c4b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4352,7 +4352,7 @@ msgstr ""
msgid "ContainerRegistry|Docker connection error"
msgstr ""
-msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token %{personalAccessTokensDocLinkEnd}instead of a password."
+msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
msgstr ""
msgid "ContainerRegistry|Last Updated"
@@ -4384,6 +4384,9 @@ msgstr ""
msgid "ContainerRegistry|Tag ID"
msgstr ""
+msgid "ContainerRegistry|There are no container images available in this group"
+msgstr ""
+
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
@@ -4393,6 +4396,9 @@ msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
+msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}"
+msgstr ""
+
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
@@ -11193,6 +11199,9 @@ msgstr ""
msgid "Number of LOCs per commit"
msgstr ""
+msgid "Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value."
+msgstr ""
+
msgid "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
msgstr ""
diff --git a/package.json b/package.json
index 29fb22d4caa..54f803a8964 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.6.2",
"@gitlab/svgs": "^1.78.0",
- "@gitlab/ui": "5.32.0",
+ "@gitlab/ui": "5.35.0",
"@gitlab/visual-review-tools": "1.0.3",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 3ccf2d62ae4..573a5ccde11 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -62,10 +62,10 @@ gitlab:
unicorn:
resources:
requests:
- cpu: 600m
+ cpu: 400m
memory: 1.4G
limits:
- cpu: 1.2G
+ cpu: 800m
memory: 2.8G
deployment:
readinessProbe:
@@ -95,10 +95,10 @@ gitlab-runner:
minio:
resources:
requests:
- cpu: 100m
+ cpu: 5m
memory: 128M
limits:
- cpu: 200m
+ cpu: 10m
memory: 280M
nginx-ingress:
controller:
@@ -107,10 +107,10 @@ nginx-ingress:
replicaCount: 2
resources:
requests:
- cpu: 150m
+ cpu: 100m
memory: 250M
limits:
- cpu: 300m
+ cpu: 200m
memory: 500M
minAvailable: 1
service:
diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb
new file mode 100644
index 00000000000..4129891914d
--- /dev/null
+++ b/spec/controllers/groups/registry/repositories_controller_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::Registry::RepositoriesController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:group, reload: true) { create(:group) }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ group.add_owner(user)
+ group.add_guest(guest)
+ sign_in(user)
+ end
+
+ context 'GET #index' do
+ context 'when container registry is enabled' do
+ it 'show index page' do
+ get :index, params: {
+ group_id: group
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'has the correct response schema' do
+ get :index, params: {
+ group_id: group,
+ format: :json
+ }
+
+ expect(response).to match_response_schema('registry/repositories')
+ end
+
+ it 'returns a list of projects for json format' do
+ project = create(:project, group: group)
+ repo = create(:container_repository, project: project)
+
+ get :index, params: {
+ group_id: group,
+ format: :json
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.first).to include(
+ 'id' => repo.id,
+ 'name' => repo.name
+ )
+ end
+
+ it 'tracks the event' do
+ expect(Gitlab::Tracking).to receive(:event).with(anything, 'list_repositories', {})
+
+ get :index, params: {
+ group_id: group
+ }
+ end
+ end
+
+ context 'container registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
+ it 'renders not found' do
+ get :index, params: {
+ group_id: group
+ }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'user do not have acces to container registry' do
+ before do
+ sign_out(user)
+ sign_in(guest)
+ end
+
+ it 'renders not found' do
+ get :index, params: {
+ group_id: group
+ }
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index 29cfe8fb295..a201ca94380 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -7,7 +7,7 @@ FactoryBot.define do
group { nil }
project_id { nil }
group_id { nil }
- parent { nil }
+ resource_parent { nil }
end
after(:build, :stub) do |board, evaluator|
@@ -19,9 +19,9 @@ FactoryBot.define do
board.project = evaluator.project
elsif evaluator.project_id
board.project_id = evaluator.project_id
- elsif evaluator.parent
- id = evaluator.parent.id
- evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
+ elsif evaluator.resource_parent
+ id = evaluator.resource_parent.id
+ evaluator.resource_parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
else
board.project = create(:project, :empty_repo)
end
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index 75ff925774a..32eee645f6a 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -9,7 +9,7 @@ FactoryBot.define do
group { nil }
project_id { nil }
group_id { nil }
- parent { nil }
+ resource_parent { nil }
end
trait :active do
@@ -34,9 +34,9 @@ FactoryBot.define do
milestone.project = evaluator.project
elsif evaluator.project_id
milestone.project_id = evaluator.project_id
- elsif evaluator.parent
- id = evaluator.parent.id
- evaluator.parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
+ elsif evaluator.resource_parent
+ id = evaluator.resource_parent.id
+ evaluator.resource_parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
else
milestone.project = create(:project)
end
diff --git a/spec/features/groups/user_sees_package_sidebar_spec.rb b/spec/features/groups/user_sees_package_sidebar_spec.rb
new file mode 100644
index 00000000000..f85b6841636
--- /dev/null
+++ b/spec/features/groups/user_sees_package_sidebar_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Groups > sidebar' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ context 'Package menu' do
+ context 'when container registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true)
+ visit group_path(group)
+ end
+
+ it 'shows main menu' do
+ within '.nav-sidebar' do
+ expect(page).to have_link(_('Packages'))
+ end
+ end
+
+ it 'has container registry link' do
+ within '.nav-sidebar' do
+ expect(page).to have_link(_('Container Registry'))
+ end
+ end
+ end
+
+ context 'when container registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ visit group_path(group)
+ end
+
+ it 'does not have container registry link' do
+ within '.nav-sidebar' do
+ expect(page).not_to have_link(_('Container Registry'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 7c1d88f7798..fd72f2dfefa 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -12,7 +12,7 @@ describe "User deletes milestone", :js do
end
context "when milestone belongs to project" do
- let!(:milestone) { create(:milestone, parent: project, title: "project milestone") }
+ let!(:milestone) { create(:milestone, resource_parent: project, title: "project milestone") }
it "deletes milestone" do
project.add_developer(user)
@@ -30,8 +30,8 @@ describe "User deletes milestone", :js do
end
context "when milestone belongs to group" do
- let!(:milestone_to_be_deleted) { create(:milestone, parent: group, title: "group milestone 1") }
- let!(:milestone) { create(:milestone, parent: group, title: "group milestone 2") }
+ let!(:milestone_to_be_deleted) { create(:milestone, resource_parent: group, title: "group milestone 1") }
+ let!(:milestone) { create(:milestone, resource_parent: group, title: "group milestone 2") }
it "deletes milestone" do
group.add_developer(user)
diff --git a/spec/finders/boards/visits_finder_spec.rb b/spec/finders/boards/visits_finder_spec.rb
index 4d40f4826f8..7e3ad8aa9f0 100644
--- a/spec/finders/boards/visits_finder_spec.rb
+++ b/spec/finders/boards/visits_finder_spec.rb
@@ -10,7 +10,7 @@ describe Boards::VisitsFinder do
let(:project) { create(:project) }
let(:project_board) { create(:board, project: project) }
- subject(:finder) { described_class.new(project_board.parent, user) }
+ subject(:finder) { described_class.new(project_board.resource_parent, user) }
it 'returns nil when there is no user' do
finder.current_user = nil
@@ -27,7 +27,7 @@ describe Boards::VisitsFinder do
it 'queries for last N visits' do
expect(BoardProjectRecentVisit).to receive(:latest).with(user, project, count: 5).once
- described_class.new(project_board.parent, user).latest(5)
+ described_class.new(project_board.resource_parent, user).latest(5)
end
end
@@ -35,7 +35,7 @@ describe Boards::VisitsFinder do
let(:group) { create(:group) }
let(:group_board) { create(:board, group: group) }
- subject(:finder) { described_class.new(group_board.parent, user) }
+ subject(:finder) { described_class.new(group_board.resource_parent, user) }
it 'returns nil when there is no user' do
finder.current_user = nil
@@ -52,7 +52,7 @@ describe Boards::VisitsFinder do
it 'queries for last N visits' do
expect(BoardGroupRecentVisit).to receive(:latest).with(user, group, count: 5).once
- described_class.new(group_board.parent, user).latest(5)
+ described_class.new(group_board.resource_parent, user).latest(5)
end
end
end
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index 6576f3d1ff2..d1ab152330e 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -80,6 +80,81 @@ describe('Jobs Store Mutations', () => {
expect(stateCopy.traceSize).toEqual(511846);
expect(stateCopy.isTraceComplete).toEqual(true);
});
+
+ describe('with new job log', () => {
+ let stateWithNewLog;
+ beforeEach(() => {
+ gon.features = gon.features || {};
+ gon.features.jobLogJson = true;
+
+ stateWithNewLog = state();
+ });
+
+ afterEach(() => {
+ gon.features.jobLogJson = false;
+ });
+
+ describe('log.lines', () => {
+ describe('when append is true', () => {
+ it('sets the parsed log ', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+ append: true,
+ size: 511846,
+ complete: true,
+ lines: [
+ {
+ offset: 1,
+ content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ },
+ ],
+ });
+
+ expect(stateWithNewLog.trace).toEqual([
+ {
+ offset: 1,
+ content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
+ lineNumber: 0,
+ },
+ ]);
+ });
+ });
+
+ describe('when it is defined', () => {
+ it('sets the parsed log ', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+ append: false,
+ size: 511846,
+ complete: true,
+ lines: [
+ { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] },
+ ],
+ });
+
+ expect(stateWithNewLog.trace).toEqual([
+ {
+ offset: 0,
+ content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
+ lineNumber: 0,
+ },
+ ]);
+ });
+ });
+
+ describe('when it is null', () => {
+ it('sets the default value', () => {
+ mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
+ append: true,
+ html,
+ size: 511846,
+ complete: false,
+ lines: null,
+ });
+
+ expect(stateWithNewLog.trace).toEqual([]);
+ });
+ });
+ });
+ });
});
describe('STOP_POLLING_TRACE', () => {
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 9890e01460e..43dacfe622c 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -291,6 +291,13 @@ describe('Jobs Store Utils', () => {
});
});
});
+
+ describe('when no data is provided', () => {
+ it('returns an empty array', () => {
+ const result = findOffsetAndRemove();
+ expect(result).toEqual([]);
+ });
+ });
});
describe('getIncrementalLineNumber', () => {
diff --git a/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap
new file mode 100644
index 00000000000..3f13b7d4d76
--- /dev/null
+++ b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Group Empty state to match the default snapshot 1`] = `
+<div
+ class="row container-message empty-state"
+>
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no container images available in this group"
+ class=""
+ src="imageUrl"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content"
+ >
+ <h4
+ class="center"
+ style=""
+ >
+ There are no container images available in this group
+ </h4>
+
+ <p
+ class="center"
+ style=""
+ >
+ <p
+ class="js-no-container-images-text"
+ >
+ With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
+ <a
+ href="help"
+ target="_blank"
+ >
+ More Information
+ </a>
+ </p>
+ </p>
+
+ <div
+ class="text-center"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap
new file mode 100644
index 00000000000..3084462f5ae
--- /dev/null
+++ b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap
@@ -0,0 +1,186 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Registry Project Empty state to match the default snapshot 1`] = `
+<div
+ class="row container-message empty-state"
+>
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no container images stored for this project"
+ class=""
+ src="imageUrl"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content"
+ >
+ <h4
+ class="center"
+ style=""
+ >
+ There are no container images stored for this project
+ </h4>
+
+ <p
+ class="center"
+ style=""
+ >
+ <p
+ class="js-no-container-images-text"
+ >
+ With the Container Registry, every project can have its own space to store its Docker images.
+ <a
+ href="help"
+ target="_blank"
+ >
+ More Information
+ </a>
+ </p>
+
+ <h5>
+ Quick Start
+ </h5>
+
+ <p
+ class="js-not-logged-in-to-registry-text"
+ >
+ If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
+ <a
+ href="help_link"
+ target="_blank"
+ >
+ Two-Factor Authentication
+ </a>
+ enabled, use a
+ <a
+ href="personal_token"
+ target="_blank"
+ >
+ Personal Access Token
+ </a>
+ instead of a password.
+ </p>
+
+ <div
+ class="input-group append-bottom-10"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-default"
+ data-clipboard-text="docker login host"
+ data-original-title="Copy login command"
+ title=""
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="s16 ic-duplicate"
+ >
+ <use
+ xlink:href="#duplicate"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+
+ <p />
+
+ <p>
+
+ You can add an image to this registry with the following commands:
+
+ </p>
+
+ <div
+ class="input-group append-bottom-10"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-default"
+ data-clipboard-text="docker build -t url ."
+ data-original-title="Copy build command"
+ title=""
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="s16 ic-duplicate"
+ >
+ <use
+ xlink:href="#duplicate"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+
+ <div
+ class="input-group"
+ >
+ <input
+ class="form-control monospace"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-default"
+ data-clipboard-text="docker push url"
+ data-original-title="Copy push command"
+ title=""
+ type="button"
+ >
+ <svg
+ aria-hidden="true"
+ class="s16 ic-duplicate"
+ >
+ <use
+ xlink:href="#duplicate"
+ />
+ </svg>
+ </button>
+ </span>
+ </div>
+ </p>
+
+ <div
+ class="text-center"
+ >
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/registry/components/app_spec.js b/spec/frontend/registry/components/app_spec.js
index 5dcb61e03b5..a69c33c246d 100644
--- a/spec/frontend/registry/components/app_spec.js
+++ b/spec/frontend/registry/components/app_spec.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
import { mount } from '@vue/test-utils';
import registry from '~/registry/components/app.vue';
import { TEST_HOST } from '../../helpers/test_constants';
@@ -7,8 +8,8 @@ describe('Registry List', () => {
let wrapper;
const findCollapsibleContainer = w => w.findAll({ name: 'CollapsibeContainerRegisty' });
- const findNoContainerImagesText = w => w.find('.js-no-container-images-text');
- const findNotLoggedInToRegistryText = w => w.find('.js-not-logged-in-to-registry-text');
+ const findProjectEmptyState = w => w.find({ name: 'ProjectEmptyState' });
+ const findGroupEmptyState = w => w.find({ name: 'GroupEmptyState' });
const findSpinner = w => w.find('.gl-spinner');
const findCharacterErrorText = w => w.find('.js-character-error-text');
@@ -25,13 +26,18 @@ describe('Registry List', () => {
const setMainEndpoint = jest.fn();
const fetchRepos = jest.fn();
+ const setIsDeleteDisabled = jest.fn();
const methods = {
setMainEndpoint,
fetchRepos,
+ setIsDeleteDisabled,
};
beforeEach(() => {
+ // This is needed due to console.error called by vue to emit a warning that stop the tests.
+ // See https://github.com/vuejs/vue-test-utils/issues/532.
+ Vue.config.silent = true;
wrapper = mount(registry, {
propsData,
computed: {
@@ -43,6 +49,12 @@ describe('Registry List', () => {
});
});
+ afterEach(() => {
+ jest.clearAllMocks();
+ Vue.config.silent = false;
+ wrapper.destroy();
+ });
+
describe('with data', () => {
it('should render a list of CollapsibeContainerRegisty', () => {
const containers = findCollapsibleContainer(wrapper);
@@ -65,18 +77,9 @@ describe('Registry List', () => {
});
});
- it('should render empty message', () => {
- const noContainerImagesText = findNoContainerImagesText(localWrapper);
- expect(noContainerImagesText.text()).toEqual(
- 'With the Container Registry, every project can have its own space to store its Docker images. More Information',
- );
- });
-
- it('should render login help text', () => {
- const notLoggedInToRegistryText = findNotLoggedInToRegistryText(localWrapper);
- expect(notLoggedInToRegistryText.text()).toEqual(
- 'If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have Two-Factor Authentication enabled, use a Personal Access Token instead of a password.',
- );
+ it('should render project empty message', () => {
+ const projectEmptyState = findProjectEmptyState(localWrapper);
+ expect(projectEmptyState.exists()).toBe(true);
});
});
@@ -129,4 +132,29 @@ describe('Registry List', () => {
);
});
});
+
+ describe('with groupId set', () => {
+ const isGroupPage = true;
+
+ beforeEach(() => {
+ wrapper = mount(registry, {
+ propsData: {
+ ...propsData,
+ endpoint: null,
+ isGroupPage,
+ },
+ methods,
+ });
+ });
+
+ it('call the right vuex setters', () => {
+ expect(methods.setMainEndpoint).toHaveBeenLastCalledWith(null);
+ expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true);
+ });
+
+ it('should render groups empty message', () => {
+ const groupEmptyState = findGroupEmptyState(wrapper);
+ expect(groupEmptyState.exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/registry/components/collapsible_container_spec.js b/spec/frontend/registry/components/collapsible_container_spec.js
index 0fe4338f1ba..f93ebab1a4d 100644
--- a/spec/frontend/registry/components/collapsible_container_spec.js
+++ b/spec/frontend/registry/components/collapsible_container_spec.js
@@ -1,24 +1,40 @@
import Vue from 'vue';
-import { mount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import { repoPropsData } from '../mock_data';
import createFlash from '~/flash';
+import * as getters from '~/registry/stores/getters';
jest.mock('~/flash.js');
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
describe('collapsible registry container', () => {
let wrapper;
+ let store;
const findDeleteBtn = w => w.find('.js-remove-repo');
const findContainerImageTags = w => w.find('.container-image-tags');
const findToggleRepos = w => w.findAll('.js-toggle-repo');
+ const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue });
+
beforeEach(() => {
createFlash.mockClear();
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
- wrapper = mount(collapsibleComponent, {
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: false,
+ },
+ getters,
+ });
+
+ wrapper = mountWithStore({
propsData: {
repo: repoPropsData,
},
@@ -27,6 +43,7 @@ describe('collapsible registry container', () => {
afterEach(() => {
Vue.config.silent = false;
+ wrapper.destroy();
});
describe('toggle', () => {
@@ -86,4 +103,25 @@ describe('collapsible registry container', () => {
});
});
});
+
+ describe('disabled delete', () => {
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: true,
+ },
+ getters,
+ });
+ wrapper = mountWithStore({
+ propsData: {
+ repo: repoPropsData,
+ },
+ });
+ });
+
+ it('should not render delete button', () => {
+ const deleteBtn = findDeleteBtn(wrapper);
+ expect(deleteBtn.exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/registry/components/group_empty_state_spec.js b/spec/frontend/registry/components/group_empty_state_spec.js
new file mode 100644
index 00000000000..f71074b5154
--- /dev/null
+++ b/spec/frontend/registry/components/group_empty_state_spec.js
@@ -0,0 +1,23 @@
+import { mount } from '@vue/test-utils';
+import groupEmptyState from '~/registry/components/group_empty_state.vue';
+
+describe('Registry Group Empty state', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(groupEmptyState, {
+ propsData: {
+ noContainersImage: 'imageUrl',
+ helpPagePath: 'help',
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/registry/components/project_empty_state_spec.js b/spec/frontend/registry/components/project_empty_state_spec.js
new file mode 100644
index 00000000000..913524db3aa
--- /dev/null
+++ b/spec/frontend/registry/components/project_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { mount } from '@vue/test-utils';
+import projectEmptyState from '~/registry/components/project_empty_state.vue';
+
+describe('Registry Project Empty state', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(projectEmptyState, {
+ propsData: {
+ noContainersImage: 'imageUrl',
+ helpPagePath: 'help',
+ repositoryUrl: 'url',
+ twoFactorAuthHelpLink: 'help_link',
+ personalAccessTokensHelpLink: 'personal_token',
+ registryHostUrlWithPort: 'host',
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/registry/components/table_registry_spec.js b/spec/frontend/registry/components/table_registry_spec.js
index 021f13feeba..600a7a6ee87 100644
--- a/spec/frontend/registry/components/table_registry_spec.js
+++ b/spec/frontend/registry/components/table_registry_spec.js
@@ -1,12 +1,19 @@
import Vue from 'vue';
+import Vuex from 'vuex';
import tableRegistry from '~/registry/components/table_registry.vue';
-import { mount } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
import { repoPropsData } from '../mock_data';
+import * as getters from '~/registry/stores/getters';
const [firstImage, secondImage] = repoPropsData.list;
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
describe('table registry', () => {
let wrapper;
+ let store;
const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
@@ -15,19 +22,31 @@ describe('table registry', () => {
const findPagination = w => w.find('.js-registry-pagination');
const bulkDeletePath = 'path';
+ const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
+
beforeEach(() => {
// This is needed due to console.error called by vue to emit a warning that stop the tests
// see https://github.com/vuejs/vue-test-utils/issues/532
Vue.config.silent = true;
- wrapper = mount(tableRegistry, {
+
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: false,
+ },
+ getters,
+ });
+
+ wrapper = mountWithStore({
propsData: {
repo: repoPropsData,
+ canDeleteRepo: true,
},
});
});
afterEach(() => {
Vue.config.silent = false;
+ wrapper.destroy();
});
describe('rendering', () => {
@@ -149,7 +168,6 @@ describe('table registry', () => {
});
describe('pagination', () => {
- let localWrapper = null;
const repo = {
repoPropsData,
pagination: {
@@ -160,7 +178,7 @@ describe('table registry', () => {
};
beforeEach(() => {
- localWrapper = mount(tableRegistry, {
+ wrapper = mount(tableRegistry, {
propsData: {
repo,
},
@@ -168,13 +186,13 @@ describe('table registry', () => {
});
it('should exist', () => {
- const pagination = findPagination(localWrapper);
+ const pagination = findPagination(wrapper);
expect(pagination.exists()).toBe(true);
});
it('should be visible when pagination is needed', () => {
- const pagination = findPagination(localWrapper);
+ const pagination = findPagination(wrapper);
expect(pagination.isVisible()).toBe(true);
- localWrapper.setProps({
+ wrapper.setProps({
repo: {
pagination: {
total: 0,
@@ -182,13 +200,13 @@ describe('table registry', () => {
},
},
});
- expect(localWrapper.vm.shouldRenderPagination).toBe(false);
+ expect(wrapper.vm.shouldRenderPagination).toBe(false);
});
it('should have a change function that update the list when run', () => {
const fetchList = jest.fn().mockResolvedValue();
- localWrapper.setMethods({ fetchList });
- localWrapper.vm.onPageChange(1);
- expect(localWrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
+ wrapper.setMethods({ fetchList });
+ wrapper.vm.onPageChange(1);
+ expect(wrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 });
});
});
@@ -208,4 +226,41 @@ describe('table registry', () => {
expect(wrapper.vm.modalDescription).toContain('<b>2</b> tags');
});
});
+
+ describe('disabled delete', () => {
+ beforeEach(() => {
+ store = new Vuex.Store({
+ state: {
+ isDeleteDisabled: true,
+ },
+ getters,
+ });
+ wrapper = mountWithStore({
+ propsData: {
+ repo: repoPropsData,
+ canDeleteRepo: false,
+ },
+ });
+ });
+
+ it('should not render select all', () => {
+ const selectAll = findSelectAllCheckbox(wrapper);
+ expect(selectAll.exists()).toBe(false);
+ });
+
+ it('should not render any select checkbox', () => {
+ const selects = findSelectCheckboxes(wrapper);
+ expect(selects.length).toBe(0);
+ });
+
+ it('should not render delete registry button', () => {
+ const deleteBtn = findDeleteButton(wrapper);
+ expect(deleteBtn.exists()).toBe(false);
+ });
+
+ it('should not render delete row button', () => {
+ const deleteBtns = findDeleteButtonsRow(wrapper);
+ expect(deleteBtns.length).toBe(0);
+ });
+ });
});
diff --git a/spec/frontend/registry/stores/actions_spec.js b/spec/frontend/registry/stores/actions_spec.js
index bf335904d23..7937fa82e80 100644
--- a/spec/frontend/registry/stores/actions_spec.js
+++ b/spec/frontend/registry/stores/actions_spec.js
@@ -34,7 +34,7 @@ describe('Actions Registry Store', () => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
});
- it('should set receveived repos', done => {
+ it('should set received repos', done => {
testAction(
actions.fetchRepos,
null,
@@ -71,10 +71,10 @@ describe('Actions Registry Store', () => {
beforeEach(() => {
state.repos = parsedReposServerResponse;
[, repo] = state.repos;
- mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
});
it('should set received list', done => {
+ mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
testAction(
actions.fetchList,
{ repo },
@@ -97,6 +97,7 @@ describe('Actions Registry Store', () => {
});
it('should create flash on API error', done => {
+ mock.onGet(repo.tagsPath).replyOnce(400);
const updatedRepo = {
...repo,
tagsPath: null,
@@ -133,6 +134,19 @@ describe('Actions Registry Store', () => {
});
});
+ describe('setIsDeleteDisabled', () => {
+ it('should commit set is delete disabled', done => {
+ testAction(
+ actions.setIsDeleteDisabled,
+ true,
+ state,
+ [{ type: types.SET_IS_DELETE_DISABLED, payload: true }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('toggleLoading', () => {
it('should commit toggle main loading', done => {
testAction(
diff --git a/spec/frontend/registry/stores/getters_spec.js b/spec/frontend/registry/stores/getters_spec.js
index 839aa718997..c16f520223b 100644
--- a/spec/frontend/registry/stores/getters_spec.js
+++ b/spec/frontend/registry/stores/getters_spec.js
@@ -7,6 +7,7 @@ describe('Getters Registry Store', () => {
state = {
isLoading: false,
endpoint: '/root/empty-project/container_registry.json',
+ isDeleteDisabled: false,
repos: [
{
canDelete: true,
@@ -43,4 +44,9 @@ describe('Getters Registry Store', () => {
expect(getters.repos(state)).toEqual(state.repos);
});
});
+ describe('isDeleteDisabled', () => {
+ it('should return isDeleteDisabled', () => {
+ expect(getters.isDeleteDisabled(state)).toEqual(state.isDeleteDisabled);
+ });
+ });
});
diff --git a/spec/frontend/registry/stores/mutations_spec.js b/spec/frontend/registry/stores/mutations_spec.js
index e19fe7a27cf..1d583028ca6 100644
--- a/spec/frontend/registry/stores/mutations_spec.js
+++ b/spec/frontend/registry/stores/mutations_spec.js
@@ -19,7 +19,16 @@ describe('Mutations Registry Store', () => {
const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo');
- expect(mockState).toEqual(expectedState);
+ expect(mockState.endpoint).toEqual(expectedState.endpoint);
+ });
+ });
+
+ describe('SET_IS_DELETE_DISABLED', () => {
+ it('should set the is delete disabled', () => {
+ const expectedState = Object.assign({}, mockState, { isDeleteDisabled: true });
+ mutations[types.SET_IS_DELETE_DISABLED](mockState, true);
+
+ expect(mockState.isDeleteDisabled).toEqual(expectedState.isDeleteDisabled);
});
});
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 98719697cea..8b33277ea18 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -191,6 +191,41 @@ describe GroupsHelper do
end
end
+ describe '#group_container_registry_nav' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+ before do
+ stub_container_registry_config(enabled: true)
+ allow(helper).to receive(:current_user) { user }
+ allow(helper).to receive(:can?).with(user, :read_container_image, group) { true }
+ helper.instance_variable_set(:@group, group)
+ end
+
+ subject { helper.group_container_registry_nav? }
+
+ context 'when container registry is enabled' do
+ it { is_expected.to be_truthy }
+
+ it 'is disabled for guest' do
+ allow(helper).to receive(:can?).with(user, :read_container_image, group) { false }
+ expect(subject).to be false
+ end
+ end
+
+ context 'when container registry is not enabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
+
+ it { is_expected.to be_falsy }
+
+ it 'is disabled for guests' do
+ allow(helper).to receive(:can?).with(user, :read_container_image, group) { false }
+ expect(subject).to be false
+ end
+ end
+ end
+
describe '#group_sidebar_links' do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
diff --git a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
new file mode 100644
index 00000000000..1be279375bd
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUserParser do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+ let(:new_user) { create(:user) }
+ let(:project) { create(:project, group: group, creator: user) }
+ let(:link) { empty_html_link }
+
+ subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+ describe '#gather_references' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = project.group.id.to_s
+ group.add_developer(new_user)
+ end
+
+ it 'returns empty list of users' do
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link has a data-project attribute' do
+ context 'using an existing project ID' do
+ before do
+ link['data-project'] = project.id.to_s
+ project.add_developer(new_user)
+ end
+
+ it 'returns empty list of users' do
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link has a data-user attribute' do
+ it 'returns an Array of users' do
+ link['data-user'] = user.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([user])
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb
new file mode 100644
index 00000000000..99d607629eb
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUsersByGroupParser do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+ let(:new_user) { create(:user) }
+ let(:project) { create(:project, group: group, creator: user) }
+ let(:link) { empty_html_link }
+
+ subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+ describe '#gather_references' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID where user does not have access' do
+ it 'returns empty array' do
+ link['data-group'] = project.group.id.to_s
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = project.group.id.to_s
+ group.add_developer(new_user)
+ end
+
+ it 'returns groups' do
+ expect(subject.gather_references([link])).to eq([group])
+ end
+ end
+
+ context 'using a non-existing group ID' do
+ it 'returns an empty Array' do
+ link['data-group'] = 'test-non-existing'
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb
new file mode 100644
index 00000000000..155f2189d9e
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MentionedUsersByProjectParser do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+ let(:new_user) { create(:user) }
+ let(:project) { create(:project, group: group, creator: user) }
+ let(:link) { empty_html_link }
+
+ subject { described_class.new(Banzai::RenderContext.new(project, new_user)) }
+
+ describe '#gather_references' do
+ context 'when the link has a data-project attribute' do
+ context 'using an existing project ID where user does not have access' do
+ it 'returns empty Array' do
+ link['data-project'] = project.id.to_s
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+
+ context 'using an existing project ID' do
+ before do
+ link['data-project'] = project.id.to_s
+ project.add_developer(new_user)
+ end
+
+ it 'returns an Array of referenced projects' do
+ expect(subject.gather_references([link])).to eq([project])
+ end
+ end
+
+ context 'using a non-existing project ID' do
+ it 'returns an empty Array' do
+ link['data-project'] = 'inexisting-project-id'
+
+ expect(subject.gather_references([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb
new file mode 100644
index 00000000000..042f9b591b6
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Config::EdgeStagesInjector do
+ describe '#call' do
+ subject { described_class.new(config).to_hash }
+
+ context 'without stages' do
+ let(:config) do
+ {
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match config }
+ end
+
+ context 'with values' do
+ let(:config) do
+ {
+ stages: %w[stage1 stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_stages) do
+ %w[.pre stage1 stage2 .post]
+ end
+
+ it { is_expected.to match(config.merge(stages: expected_stages)) }
+ end
+
+ context 'with bad values' do
+ let(:config) do
+ {
+ stages: 'stage1',
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match(config) }
+ end
+
+ context 'with collision values' do
+ let(:config) do
+ {
+ stages: %w[.post stage1 .pre .post stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_stages) do
+ %w[.pre stage1 stage2 .post]
+ end
+
+ it { is_expected.to match(config.merge(stages: expected_stages)) }
+ end
+
+ context 'with types' do
+ let(:config) do
+ {
+ types: %w[stage1 stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_config) do
+ {
+ types: %w[.pre stage1 stage2 .post],
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match expected_config }
+ end
+
+ context 'with types' do
+ let(:config) do
+ {
+ types: %w[.post stage1 .pre .post stage2],
+ test: { script: 'test' }
+ }
+ end
+
+ let(:expected_config) do
+ {
+ types: %w[.pre stage1 stage2 .post],
+ test: { script: 'test' }
+ }
+ end
+
+ it { is_expected.to match expected_config }
+ end
+ end
+
+ describe '.wrap_stages' do
+ subject { described_class.wrap_stages(stages) }
+
+ context 'with empty value' do
+ let(:stages) {}
+
+ it { is_expected.to eq %w[.pre .post] }
+ end
+
+ context 'with values' do
+ let(:stages) { %w[s1 .pre] }
+
+ it { is_expected.to eq %w[.pre s1 .post] }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 968dbb9c7f2..7e1a80414d4 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -215,7 +215,7 @@ describe Gitlab::Ci::Config::Entry::Root do
describe '#stages_value' do
it 'returns an array of root stages' do
- expect(root.stages_value).to eq %w[build test deploy]
+ expect(root.stages_value).to eq %w[.pre build test deploy .post]
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/stages_spec.rb b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
index 97970522104..3e6ff8eca28 100644
--- a/spec/lib/gitlab/ci/config/entry/stages_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::Entry::Stages do
describe '.default' do
it 'returns default stages' do
- expect(described_class.default).to eq %w[build test deploy]
+ expect(described_class.default).to eq %w[.pre build test deploy .post]
end
end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 68c38644b5c..b254f9af2f1 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -51,6 +51,54 @@ describe Gitlab::Ci::Config do
end
end
end
+
+ describe '#stages' do
+ subject(:subject) { config.stages }
+
+ context 'with default stages' do
+ let(:default_stages) do
+ %w[.pre build test deploy .post]
+ end
+
+ it { is_expected.to eq default_stages }
+ end
+
+ context 'with custom stages' do
+ let(:yml) do
+ <<-EOS
+ stages:
+ - stage1
+ - stage2
+ job1:
+ stage: stage1
+ script:
+ - ls
+ EOS
+ end
+
+ it { is_expected.to eq %w[.pre stage1 stage2 .post] }
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(ci_pre_post_pipeline_stages: false)
+ end
+
+ let(:yml) do
+ <<-EOS
+ stages:
+ - stage1
+ - stage2
+ job1:
+ stage: stage1
+ script:
+ - ls
+ EOS
+ end
+
+ it { is_expected.to eq %w[stage1 stage2] }
+ end
+ end
end
context 'when using extendable hash' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d43eb4e4b4a..cb5ebde16d7 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -26,7 +26,7 @@ module Gitlab
it 'returns valid build attributes' do
expect(subject).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -56,7 +56,7 @@ module Gitlab
it 'returns valid build attributes' do
expect(subject).to eq({
stage: 'test',
- stage_idx: 1,
+ stage_idx: 2,
name: 'rspec',
options: { script: ['rspec'] },
rules: [
@@ -209,13 +209,16 @@ module Gitlab
end
let(:attributes) do
- [{ name: "build",
+ [{ name: ".pre",
index: 0,
builds: [] },
- { name: "test",
+ { name: "build",
index: 1,
+ builds: [] },
+ { name: "test",
+ index: 2,
builds:
- [{ stage_idx: 1,
+ [{ stage_idx: 2,
stage: "test",
name: "rspec",
allow_failure: false,
@@ -225,9 +228,9 @@ module Gitlab
only: { refs: ["branches"] },
except: {} }] },
{ name: "deploy",
- index: 2,
+ index: 3,
builds:
- [{ stage_idx: 2,
+ [{ stage_idx: 3,
stage: "deploy",
name: "prod",
allow_failure: false,
@@ -235,7 +238,10 @@ module Gitlab
yaml_variables: [],
options: { script: ["cap prod"] },
only: { refs: ["tags"] },
- except: {} }] }]
+ except: {} }] },
+ { name: ".post",
+ index: 4,
+ builds: [] }]
end
it 'returns stages seed attributes' do
@@ -425,7 +431,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -456,7 +462,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -485,7 +491,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -510,7 +516,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -977,7 +983,7 @@ module Gitlab
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "rspec",
options: {
before_script: ["pwd"],
@@ -1272,7 +1278,7 @@ module Gitlab
expect(subject.builds.size).to eq(5)
expect(subject.builds[0]).to eq(
stage: "build",
- stage_idx: 0,
+ stage_idx: 1,
name: "build1",
options: {
script: ["test"]
@@ -1283,7 +1289,7 @@ module Gitlab
)
expect(subject.builds[2]).to eq(
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "test1",
options: {
script: ["test"],
@@ -1398,7 +1404,7 @@ module Gitlab
expect(subject.size).to eq(1)
expect(subject.first).to eq({
stage: "test",
- stage_idx: 1,
+ stage_idx: 2,
name: "normal_job",
options: {
script: ["test"]
@@ -1442,7 +1448,7 @@ module Gitlab
expect(subject.size).to eq(2)
expect(subject.first).to eq({
stage: "build",
- stage_idx: 0,
+ stage_idx: 1,
name: "job1",
options: {
script: ["execute-script-for-job"]
@@ -1453,7 +1459,7 @@ module Gitlab
})
expect(subject.second).to eq({
stage: "build",
- stage_idx: 0,
+ stage_idx: 1,
name: "job2",
options: {
script: ["execute-script-for-job"]
@@ -1665,14 +1671,14 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, deploy, .post")
end
it "returns errors if job stage is not a defined stage" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, .post")
end
it "returns errors if stages is not an array" do
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index e8a9f0b06a8..58509b69463 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -90,4 +90,12 @@ describe Gitlab::DataBuilder::Push do
.not_to raise_error
end
end
+
+ describe '.build_bulk' do
+ subject do
+ described_class.build_bulk(action: :created, ref_type: :branch, changes: [double, double])
+ end
+
+ it { is_expected.to eq(action: :created, ref_count: 2, ref_type: :branch) }
+ end
end
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 6d614c6527a..8331f0b6bc7 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -311,10 +311,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
end
end
- it 'creates the merge request diffs' do
+ it 'creates a merge request diff and sets it as the latest' do
mr = insert_git_data
expect(mr.merge_request_diffs.exists?).to eq(true)
+ expect(mr.reload.latest_merge_request_diff_id).to eq(mr.merge_request_diffs.first.id)
end
it 'creates the merge request diff commits' do
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index fa47cfd519b..8401b683fd5 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -370,5 +370,33 @@ describe Gitlab::Gpg::Commit do
it_behaves_like 'returns the cached signature on second call'
end
+
+ context 'multiple commits with signatures' do
+ let(:first_signature) { create(:gpg_signature) }
+
+ let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
+ let(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+
+ let!(:first_commit) { create(:commit, project: project, sha: first_signature.commit_sha) }
+ let!(:second_commit) { create(:commit, project: project, sha: second_signature.commit_sha) }
+
+ let(:commits) do
+ [first_commit, second_commit].map do |commit|
+ gpg_commit = described_class.new(commit)
+
+ allow(gpg_commit).to receive(:has_signature?).and_return(true)
+
+ gpg_commit
+ end
+ end
+
+ it 'does an aggregated sql request instead of 2 separate ones' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ commits.each(&:signature)
+ end
+
+ expect(recorder.count).to eq(1)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import/merge_request_creator_spec.rb b/spec/lib/gitlab/import/merge_request_creator_spec.rb
index 7c73e9b39f7..ff2c3032dbf 100644
--- a/spec/lib/gitlab/import/merge_request_creator_spec.rb
+++ b/spec/lib/gitlab/import/merge_request_creator_spec.rb
@@ -21,8 +21,11 @@ describe Gitlab::Import::MergeRequestCreator do
subject.execute(attributes)
- expect(merge_request.reload.merge_request_diffs.count).to eq(1)
- expect(merge_request.reload.merge_request_diffs.first.commits.count).to eq(commits_count)
+ merge_request.reload
+
+ expect(merge_request.merge_request_diffs.count).to eq(1)
+ expect(merge_request.merge_request_diffs.first.commits.count).to eq(commits_count)
+ expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.first.id)
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index d3b51a53ede..ebc5d9d1f56 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -47,6 +47,7 @@ PushEventPayload:
- commit_to
- ref
- commit_title
+- ref_count
Note:
- id
- note
diff --git a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
new file mode 100644
index 00000000000..a415b6407d5
--- /dev/null
+++ b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Exporter::SidekiqExporter do
+ let(:exporter) { described_class.new }
+
+ after do
+ exporter.stop
+ end
+
+ context 'with valid config' do
+ before do
+ stub_config(
+ monitoring: {
+ sidekiq_exporter: {
+ enabled: true,
+ port: 0,
+ address: '127.0.0.1'
+ }
+ }
+ )
+ end
+
+ it 'does start thread' do
+ expect(exporter.start).not_to be_nil
+ end
+ end
+
+ context 'when port is already taken' do
+ let(:first_exporter) { described_class.new }
+
+ before do
+ stub_config(
+ monitoring: {
+ sidekiq_exporter: {
+ enabled: true,
+ port: 9992,
+ address: '127.0.0.1'
+ }
+ }
+ )
+
+ first_exporter.start
+ end
+
+ after do
+ first_exporter.stop
+ end
+
+ it 'does print error message' do
+ expect(Sidekiq.logger).to receive(:error)
+ .with(
+ class: described_class.to_s,
+ message: 'Cannot start sidekiq_exporter',
+ exception: anything)
+
+ exporter.start
+ end
+
+ it 'does not start thread' do
+ expect(exporter.start).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7513dbeeb6f..f6ace0d8bf5 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -265,7 +265,8 @@ describe Gitlab::ReferenceExtractor do
describe 'referables prefixes' do
def prefixes
described_class::REFERABLES.each_with_object({}) do |referable, result|
- klass = referable.to_s.camelize.constantize
+ class_name = referable.to_s.camelize
+ klass = class_name.constantize if Object.const_defined?(class_name)
next unless klass.respond_to?(:reference_prefix)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 702a6fab0e6..7bef3d30064 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -60,6 +60,10 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value('three').for(:push_event_hooks_limit) }
it { is_expected.not_to allow_value(nil).for(:push_event_hooks_limit) }
+ it { is_expected.to allow_value(3).for(:push_event_activities_limit) }
+ it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
+ it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
+
context "when user accepted let's encrypt terms of service" do
before do
setting.update(lets_encrypt_terms_of_service_accepted: true)
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index 4911375c962..dd18c8842ab 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -60,6 +60,18 @@ RSpec.describe GpgSignature do
end
end
+ describe '.by_commit_sha scope' do
+ let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
+ let!(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+
+ it 'returns all gpg signatures by sha' do
+ expect(described_class.by_commit_sha(commit_sha)).to eq([gpg_signature])
+ expect(
+ described_class.by_commit_sha([commit_sha, another_gpg_signature.commit_sha])
+ ).to contain_exactly(gpg_signature, another_gpg_signature)
+ end
+ end
+
describe '#commit' do
it 'fetches the commit through the project' do
expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit)
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 83c7464757f..8a47b8c206b 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -972,13 +972,13 @@ describe Note do
project = create(:project)
note = create(:note_on_issue, project: project)
- expect(note.parent).to eq(project)
+ expect(note.resource_parent).to eq(project)
end
it 'returns nil for personal snippet note' do
note = create(:note_on_personal_snippet)
- expect(note.parent).to be_nil
+ expect(note.resource_parent).to be_nil
end
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 2fc772b12af..992fd5e9c66 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -122,6 +122,7 @@ describe API::Events do
expect(payload_hash['action']).to eq(payload.action)
expect(payload_hash['ref_type']).to eq(payload.ref_type)
expect(payload_hash['commit_to']).to eq(payload.commit_to)
+ expect(payload_hash['ref_count']).to eq(payload.ref_count)
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index af1cf80e9d3..f3bfb258029 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -73,7 +73,8 @@ describe API::Settings, 'Settings' do
local_markdown_version: 3,
allow_local_requests_from_web_hooks_and_services: true,
allow_local_requests_from_system_hooks: false,
- push_event_hooks_limit: 2
+ push_event_hooks_limit: 2,
+ push_event_activities_limit: 2
}
expect(response).to have_gitlab_http_status(200)
@@ -104,6 +105,7 @@ describe API::Settings, 'Settings' do
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
expect(json_response['allow_local_requests_from_system_hooks']).to eq(false)
expect(json_response['push_event_hooks_limit']).to eq(2)
+ expect(json_response['push_event_activities_limit']).to eq(2)
end
end
diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb
index 5848dd64c3b..799a8d5c122 100644
--- a/spec/serializers/container_repository_entity_spec.rb
+++ b/spec/serializers/container_repository_entity_spec.rb
@@ -25,6 +25,18 @@ describe ContainerRepositoryEntity do
expect(subject).to include(:id, :path, :location, :tags_path)
end
+ context 'when project is not preset in the request' do
+ before do
+ allow(request).to receive(:respond_to?).and_return(false)
+ allow(request).to receive(:project).and_return(nil)
+ end
+
+ it 'uses project from the object' do
+ expect(request.project).not_to equal(project)
+ expect(subject).to include(:tags_path)
+ end
+ end
+
context 'when user can manage repositories' do
before do
project.add_developer(user)
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index 33637419f83..ef7b7fdbaac 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Issues::CreateService do
let(:label) { create(:label, project: project, name: 'in-progress') }
let!(:list) { create(:list, board: board, label: label, position: 0) }
- subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
+ subject(:service) { described_class.new(board.resource_parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
before do
project.add_developer(user)
diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb
index a5411a2fb3a..243e0fc50ad 100644
--- a/spec/services/boards/lists/update_service_spec.rb
+++ b/spec/services/boards/lists/update_service_spec.rb
@@ -9,9 +9,9 @@ describe Boards::Lists::UpdateService do
shared_examples 'moving list' do
context 'when user can admin list' do
it 'calls Lists::MoveService to update list position' do
- board.parent.add_developer(user)
+ board.resource_parent.add_developer(user)
- expect(Boards::Lists::MoveService).to receive(:new).with(board.parent, user, params).and_call_original
+ expect(Boards::Lists::MoveService).to receive(:new).with(board.resource_parent, user, params).and_call_original
expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list)
service.execute(list)
@@ -30,7 +30,7 @@ describe Boards::Lists::UpdateService do
shared_examples 'updating list preferences' do
context 'when user can read list' do
it 'updates list preference for user' do
- board.parent.add_guest(user)
+ board.resource_parent.add_guest(user)
service.execute(list)
@@ -48,7 +48,7 @@ describe Boards::Lists::UpdateService do
end
describe '#execute' do
- let(:service) { described_class.new(board.parent, user, params) }
+ let(:service) { described_class.new(board.resource_parent, user, params) }
context 'when position parameter is present' do
let(:params) { { position: 1 } }
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
index 6baf7ac9deb..203c287f396 100644
--- a/spec/services/boards/visits/create_service_spec.rb
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Visits::CreateService do
let(:project) { create(:project) }
let(:project_board) { create(:board, project: project) }
- subject(:service) { described_class.new(project_board.parent, user) }
+ subject(:service) { described_class.new(project_board.resource_parent, user) }
it 'returns nil when there is no user' do
service.current_user = nil
@@ -35,7 +35,7 @@ describe Boards::Visits::CreateService do
let(:group) { create(:group) }
let(:group_board) { create(:board, group: group) }
- subject(:service) { described_class.new(group_board.parent, user) }
+ subject(:service) { described_class.new(group_board.resource_parent, user) }
it 'returns nil when there is no user' do
service.current_user = nil
diff --git a/spec/services/bulk_push_event_payload_service_spec.rb b/spec/services/bulk_push_event_payload_service_spec.rb
new file mode 100644
index 00000000000..661c3540aa0
--- /dev/null
+++ b/spec/services/bulk_push_event_payload_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BulkPushEventPayloadService do
+ let(:event) { create(:push_event) }
+
+ let(:push_data) do
+ {
+ action: :created,
+ ref_count: 4,
+ ref_type: :branch
+ }
+ end
+
+ subject { described_class.new(event, push_data) }
+
+ it 'creates a PushEventPayload' do
+ push_event_payload = subject.execute
+
+ expect(push_event_payload).to be_persisted
+ expect(push_event_payload.action).to eq(push_data[:action].to_s)
+ expect(push_event_payload.commit_count).to eq(0)
+ expect(push_event_payload.ref_count).to eq(push_data[:ref_count])
+ expect(push_event_payload.ref_type).to eq(push_data[:ref_type].to_s)
+ end
+end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 9f2c3fec62c..eb738ac80b1 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -113,40 +113,21 @@ describe EventCreateService do
end
end
- describe '#push', :clean_gitlab_redis_shared_state do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- let(:push_data) do
- {
- commits: [
- {
- id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- message: 'This is a commit'
- }
- ],
- before: '0000000000000000000000000000000000000000',
- after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
- total_commits_count: 1,
- ref: 'refs/heads/my-branch'
- }
- end
-
+ shared_examples_for 'service for creating a push event' do |service_class|
it 'creates a new event' do
- expect { service.push(project, user, push_data) }.to change { Event.count }
+ expect { subject }.to change { Event.count }
end
it 'creates the push event payload' do
- expect(PushEventPayloadService).to receive(:new)
+ expect(service_class).to receive(:new)
.with(an_instance_of(PushEvent), push_data)
.and_call_original
- service.push(project, user, push_data)
+ subject
end
it 'updates user last activity' do
- expect { service.push(project, user, push_data) }
- .to change { user.last_activity_on }.to(Date.today)
+ expect { subject }.to change { user.last_activity_on }.to(Date.today)
end
it 'caches the last push event for the user' do
@@ -154,7 +135,7 @@ describe EventCreateService do
.to receive(:cache_last_push_event)
.with(an_instance_of(PushEvent))
- service.push(project, user, push_data)
+ subject
end
it 'does not create any event data when an error is raised' do
@@ -163,17 +144,56 @@ describe EventCreateService do
allow(payload_service).to receive(:execute)
.and_raise(RuntimeError)
- allow(PushEventPayloadService).to receive(:new)
+ allow(service_class).to receive(:new)
.and_return(payload_service)
- expect { service.push(project, user, push_data) }
- .to raise_error(RuntimeError)
-
+ expect { subject }.to raise_error(RuntimeError)
expect(Event.count).to eq(0)
expect(PushEventPayload.count).to eq(0)
end
end
+ describe '#push', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ let(:push_data) do
+ {
+ commits: [
+ {
+ id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ message: 'This is a commit'
+ }
+ ],
+ before: '0000000000000000000000000000000000000000',
+ after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ total_commits_count: 1,
+ ref: 'refs/heads/my-branch'
+ }
+ end
+
+ subject { service.push(project, user, push_data) }
+
+ it_behaves_like 'service for creating a push event', PushEventPayloadService
+ end
+
+ describe '#bulk_push', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ let(:push_data) do
+ {
+ action: :created,
+ ref_count: 4,
+ ref_type: :branch
+ }
+ end
+
+ subject { service.bulk_push(project, user, push_data) }
+
+ it_behaves_like 'service for creating a push event', BulkPushEventPayloadService
+ end
+
describe 'Project' do
let(:user) { create :user }
let(:project) { create(:project) }
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index 90b3eb38469..f3f6b36a18d 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -12,8 +12,8 @@ describe Git::BaseHooksService do
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
- describe '#execute_project_hooks' do
- class TestService < described_class
+ let(:test_service) do
+ Class.new(described_class) do
def hook_name
:push_hooks
end
@@ -22,22 +22,44 @@ describe Git::BaseHooksService do
[]
end
end
+ end
- let(:project) { create(:project, :repository) }
+ subject { test_service.new(project, user, params) }
- let(:params) do
- {
- change: {
- oldrev: oldrev,
- newrev: newrev,
- ref: ref
- }
+ let(:params) do
+ {
+ change: {
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
}
+ }
+ end
+
+ describe 'push event' do
+ it 'creates push event' do
+ expect_next_instance_of(EventCreateService) do |service|
+ expect(service).to receive(:push)
+ end
+
+ subject.execute
end
- subject { TestService.new(project, user, params) }
+ context 'create_push_event is set to false' do
+ before do
+ params[:create_push_event] = false
+ end
+
+ it 'does not create push event' do
+ expect(EventCreateService).not_to receive(:new)
+
+ subject.execute
+ end
+ end
+ end
- context '#execute_hooks' do
+ describe 'project hooks and services' do
+ context 'hooks' do
before do
expect(project).to receive(:has_active_hooks?).and_return(active)
end
@@ -65,7 +87,7 @@ describe Git::BaseHooksService do
end
end
- context '#execute_services' do
+ context 'services' do
before do
expect(project).to receive(:has_active_services?).and_return(active)
end
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index eeb395f6c7b..35ddf95b5f6 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -13,6 +13,12 @@ describe Git::ProcessRefChangesService do
let(:service) { double(execute: true) }
let(:git_changes) { double(branch_changes: [], tag_changes: []) }
+ def multiple_changes(change, count)
+ Array.new(count).map.with_index do |n, index|
+ { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" }
+ end
+ end
+
let(:changes) do
[
{ index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
@@ -28,7 +34,7 @@ describe Git::ProcessRefChangesService do
it "calls #{push_service_class}" do
expect(push_service_class)
.to receive(:new)
- .with(project, project.owner, hash_including(execute_project_hooks: true))
+ .with(project, project.owner, hash_including(execute_project_hooks: true, create_push_event: true))
.exactly(changes.count).times
.and_return(service)
@@ -36,12 +42,6 @@ describe Git::ProcessRefChangesService do
end
context 'changes exceed push_event_hooks_limit' do
- def multiple_changes(change, count)
- Array.new(count).map.with_index do |n, index|
- { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" }
- end
- end
-
let(:push_event_hooks_limit) { 3 }
let(:changes) do
@@ -88,6 +88,40 @@ describe Git::ProcessRefChangesService do
end
end
+ context 'changes exceed push_event_activities_limit per action' do
+ let(:push_event_activities_limit) { 3 }
+
+ let(:changes) do
+ [
+ { oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
+ { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" },
+ { oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" }
+ ].map do |change|
+ multiple_changes(change, push_event_activities_limit + 1)
+ end.flatten
+ end
+
+ before do
+ stub_application_setting(push_event_activities_limit: push_event_activities_limit)
+ end
+
+ it "calls #{push_service_class} with create_push_event set to false" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(create_push_event: false))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+
+ it 'creates events per action' do
+ allow(push_service_class).to receive(:new).and_return(service)
+
+ expect { subject.execute }.to change { Event.count }.by(3)
+ end
+ end
+
context 'pipeline creation' do
context 'with valid .gitlab-ci.yml' do
before do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index ec68e1a8cf9..788f83cc233 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1545,12 +1545,20 @@ describe QuickActions::InterpretService do
end
it 'limits to commands passed ' do
- content = "/shrug\n/close"
+ content = "/shrug test\n/close"
text, commands = service.execute(content, issue, only: [:shrug])
expect(commands).to be_empty
- expect(text).to eq("#{described_class::SHRUG}\n/close")
+ expect(text).to eq("test #{described_class::SHRUG}\n/close")
+ end
+
+ it 'preserves leading whitespace ' do
+ content = " - list\n\n/close\n\ntest\n\n"
+
+ text, _ = service.execute(content, issue)
+
+ expect(text).to eq(" - list\n\ntest")
end
context '/create_merge_request command' do
diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb
index b7aff32460d..d41490f33e4 100644
--- a/spec/support/api/boards_shared_examples.rb
+++ b/spec/support/api/boards_shared_examples.rb
@@ -171,7 +171,7 @@ shared_examples_for 'group and project boards' do |route_definition, ee = false|
if board_parent.try(:namespace)
board_parent.update(namespace: owner.namespace)
else
- board.parent.add_owner(owner)
+ board.resource_parent.add_owner(owner)
end
end
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index d6439f77408..ce8c2140e99 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -205,7 +205,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
describe "DELETE #{route_definition}/:milestone_id" do
it "rejects a member with reporter access from deleting a milestone" do
reporter = create(:user)
- milestone.parent.add_reporter(reporter)
+ milestone.resource_parent.add_reporter(reporter)
delete api(resource_route, reporter)
diff --git a/spec/support/shared_examples/services/boards/boards_create_service.rb b/spec/support/shared_examples/services/boards/boards_create_service.rb
index 19818a6091b..7fd69354c2d 100644
--- a/spec/support/shared_examples/services/boards/boards_create_service.rb
+++ b/spec/support/shared_examples/services/boards/boards_create_service.rb
@@ -17,7 +17,7 @@ shared_examples 'boards create service' do
context 'when parent has a board' do
before do
- create(:board, parent: parent)
+ create(:board, resource_parent: parent)
end
it 'does not create a new board' do
diff --git a/spec/support/shared_examples/services/boards/boards_list_service.rb b/spec/support/shared_examples/services/boards/boards_list_service.rb
index 566e5050f8e..25dc2e04942 100644
--- a/spec/support/shared_examples/services/boards/boards_list_service.rb
+++ b/spec/support/shared_examples/services/boards/boards_list_service.rb
@@ -15,7 +15,7 @@ shared_examples 'boards list service' do
context 'when parent has a board' do
before do
- create(:board, parent: parent)
+ create(:board, resource_parent: parent)
end
it 'does not create a new board' do
@@ -24,7 +24,7 @@ shared_examples 'boards list service' do
end
it 'returns parent boards' do
- board = create(:board, parent: parent)
+ board = create(:board, resource_parent: parent)
expect(service.execute).to eq [board]
end
diff --git a/spec/views/events/event/_push.html.haml_spec.rb b/spec/views/events/event/_push.html.haml_spec.rb
index e43e37188a3..d33a8aa86fc 100644
--- a/spec/views/events/event/_push.html.haml_spec.rb
+++ b/spec/views/events/event/_push.html.haml_spec.rb
@@ -28,6 +28,23 @@ describe 'events/event/_push.html.haml' do
expect(rendered).not_to have_link(event.ref_name)
end
end
+
+ context 'ref_count is more than 1' do
+ let(:payload) do
+ build_stubbed(
+ :push_event_payload,
+ event: event,
+ ref_count: 4,
+ ref_type: :branch
+ )
+ end
+
+ it 'includes the count in the text' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to include('4 branches')
+ end
+ end
end
context 'with a tag' do
@@ -53,5 +70,22 @@ describe 'events/event/_push.html.haml' do
expect(rendered).not_to have_link(event.ref_name)
end
end
+
+ context 'ref_count is more than 1' do
+ let(:payload) do
+ build_stubbed(
+ :push_event_payload,
+ event: event,
+ ref_count: 4,
+ ref_type: :tag
+ )
+ end
+
+ it 'includes the count in the text' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to include('4 tags')
+ end
+ end
end
end
diff --git a/yarn.lock b/yarn.lock
index 45375114d43..fbaddce43c6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -995,10 +995,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.78.0.tgz#469493bd6cdd254eb5d1271edeab22bbbee2f4c4"
integrity sha512-dBgEB/Q4FRD0NapmNrD86DF1FsV0uSgTx0UOJloHnGE2DNR2P1HQrCmLW2fX+QgN4P9CDAzdi2buVHuholofWw==
-"@gitlab/ui@5.32.0":
- version "5.32.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.32.0.tgz#21bb70b6c8b68bdcbb53ffebde80ff3cd93851c8"
- integrity sha512-xTFz4/WbR1e6zj2xI2DULcAGicA6qidb9Reoa02V5snqWcQY+iHDup/XzgXmttTPCiBlqPIFo/CMhH4gSJWuPQ==
+"@gitlab/ui@5.35.0":
+ version "5.35.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.35.0.tgz#843e9febf1d4ef9b846dc3280e32e3e626c6f9b1"
+ integrity sha512-PD9hqVlRhwYRPbL+u/gcHew8NfPXbphZ0CQqfIXaWUYdEOMksUtP6DnLToG6S321WjrCMD+IBHnVQxf2juZBxg==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -1008,6 +1008,7 @@
highlight.js "^9.13.1"
js-beautify "^1.8.8"
lodash "^4.17.14"
+ resize-observer-polyfill "^1.5.1"
url-search-params-polyfill "^5.0.0"
vue "^2.6.10"
vue-loader "^15.4.2"
@@ -10531,6 +10532,11 @@ requizzle@~0.2.1:
dependencies:
underscore "~1.6.0"
+resize-observer-polyfill@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"