summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md26
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js2
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js6
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue4
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue16
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue11
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue7
-rw-r--r--app/assets/javascripts/ide/stores/actions.js12
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js9
-rw-r--r--app/assets/javascripts/ide/stores/utils.js4
-rw-r--r--app/assets/javascripts/ide/utils.js2
-rw-r--r--app/assets/javascripts/manual_ordering.js58
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue77
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js2
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue49
-rw-r--r--app/assets/javascripts/repository/graphql.js17
-rw-r--r--app/assets/javascripts/repository/index.js1
-rw-r--r--app/assets/javascripts/repository/log_tree.js64
-rw-r--r--app/assets/javascripts/repository/queries/getCommit.query.graphql10
-rw-r--r--app/assets/javascripts/repository/queries/getCommits.query.graphql10
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue3
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/assets/stylesheets/pages/issues.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/controllers/concerns/continue_params.rb2
-rw-r--r--app/controllers/concerns/internal_redirect.rb4
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb4
-rw-r--r--app/controllers/groups_controller.rb4
-rw-r--r--app/controllers/projects/forks_controller.rb18
-rw-r--r--app/controllers/projects/imports_controller.rb8
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/refs_controller.rb1
-rw-r--r--app/controllers/registrations_controller.rb30
-rw-r--r--app/helpers/issues_helper.rb1
-rw-r--r--app/helpers/recaptcha_experiment_helper.rb7
-rw-r--r--app/models/broadcast_message.rb2
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/namespace/aggregation_schedule.rb7
-rw-r--r--app/models/namespace/root_storage_statistics.rb10
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb10
-rw-r--r--app/services/projects/propagate_service_template.rb2
-rw-r--r--app/views/admin/application_settings/_spam.html.haml5
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml3
-rw-r--r--changelogs/unreleased/51952-forking-via-webide.yml5
-rw-r--r--changelogs/unreleased/58802-rename-webide.yml5
-rw-r--r--changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml5
-rw-r--r--changelogs/unreleased/62938-wcag-aa-edited-text-color.yml5
-rw-r--r--changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml5
-rw-r--r--changelogs/unreleased/always-allow-prometheus-access-in-dev.yml5
-rw-r--r--changelogs/unreleased/always-display-environment-selector.yml5
-rw-r--r--changelogs/unreleased/bug-63162-duplicate_path_in_links.yml5
-rw-r--r--changelogs/unreleased/fe-issue-reorder.yml5
-rw-r--r--changelogs/unreleased/fix-jupyter-git-v3.yml5
-rw-r--r--changelogs/unreleased/fix-labels-in-hooks.yml5
-rw-r--r--changelogs/unreleased/fix-notes-emails-with-group-settings.yml5
-rw-r--r--changelogs/unreleased/sh-cache-negative-entries-find-commit.yml5
-rw-r--r--changelogs/unreleased/sh-omit-issues-links-on-poll.yml5
-rw-r--r--changelogs/unreleased/sh-quiet-backup-secrets-log.yml5
-rw-r--r--changelogs/unreleased/sh-recover-ee-schema-backport-migration-failure.yml5
-rw-r--r--changelogs/unreleased/sh-service-template-bug.yml5
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--db/migrate/20190531153110_create_namespace_root_storage_statistics.rb22
-rw-r--r--db/migrate/20190605184422_create_namespace_aggregation_schedules.rb14
-rw-r--r--db/schema.rb17
-rw-r--r--doc/administration/high_availability/gitlab.md11
-rw-r--r--doc/administration/high_availability/pgbouncer.md27
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/docker/using_docker_build.md9
-rw-r--r--doc/development/api_graphql_styleguide.md2
-rw-r--r--doc/development/documentation/styleguide.md21
-rw-r--r--doc/development/testing_guide/frontend_testing.md13
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md10
-rw-r--r--doc/user/profile/account/two_factor_authentication.md5
-rw-r--r--doc/user/project/pages/getting_started_part_three.md39
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb40
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb12
-rw-r--r--lib/gitlab/json_cache.rb2
-rw-r--r--locale/gitlab.pot20
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb8
-rw-r--r--spec/controllers/concerns/continue_params_spec.rb8
-rw-r--r--spec/controllers/concerns/internal_redirect_spec.rb77
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb16
-rw-r--r--spec/controllers/registrations_controller_spec.rb17
-rw-r--r--spec/factories/namespace/aggregation_schedules.rb7
-rw-r--r--spec/factories/namespace/root_storage_statistics.rb7
-rw-r--r--spec/factories/namespaces.rb8
-rw-r--r--spec/features/groups/issues_spec.rb52
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb39
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb40
-rw-r--r--spec/features/users/signup_spec.rb2
-rw-r--r--spec/frontend/boards/modal_store_spec.js2
-rw-r--r--spec/frontend/boards/services/board_service_spec.js390
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js16
-rw-r--r--spec/frontend/ide/utils_spec.js44
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap14
-rw-r--r--spec/frontend/repository/components/table/row_spec.js2
-rw-r--r--spec/frontend/repository/log_tree_spec.js129
-rw-r--r--spec/helpers/recaptcha_experiment_helper_spec.rb23
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js14
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js37
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js10
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js11
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js46
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb13
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb51
-rw-r--r--spec/lib/gitlab/json_cache_spec.rb35
-rw-r--r--spec/models/broadcast_message_spec.rb6
-rw-r--r--spec/models/internal_id_spec.rb2
-rw-r--r--spec/models/namespace/aggregation_schedule_spec.rb7
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb10
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb12
-rw-r--r--spec/services/projects/propagate_service_template_spec.rb2
-rw-r--r--spec/spec_helper.rb6
-rw-r--r--spec/support/helpers/prometheus_helpers.rb4
-rw-r--r--spec/support/inspect_squelch.rb7
-rw-r--r--spec/support/sidekiq.rb4
-rw-r--r--vendor/jupyter/values.yaml2
132 files changed, 1775 insertions, 325 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 118034867ad..8d4509e370d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 12.0.2 (2019-06-25)
+
+### Fixed (7 changes, 1 of them is from the community)
+
+- Fix missing API notification flags for Microsoft Teams. !29824 (Seiji Suenaga)
+- Fixed 'diff version changes' link not working. !29825
+- Fix label serialization in issue and note hooks. !29850
+- Include the GitLab version in the cache key for Gitlab::JsonCache. !29938
+- Prevent EE backport migrations from running if CE is not migrated. !30002
+- Silence backup warnings when CRON=1 in use. !30033
+- Fix comment emails not respecting group-level notification email.
+
+### Performance (1 change)
+
+- Omit issues links in merge request entity API response. !29917
+
+
## 12.0.1 (2019-06-24)
- No changes.
@@ -316,6 +333,15 @@ entry.
- Moves snowplow to CE repo.
+## 11.11.4 (2019-06-26)
+
+### Fixed (3 changes)
+
+- Fix Fogbugz Importer not working. !29383
+- Fix scrolling to top on assignee change. !29500
+- Fix IDE commit using latest ref in branch and overriding contents. !29769
+
+
## 11.11.3 (2019-06-10)
### Fixed (5 changes)
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index 983b28d2e67..636ca99952c 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,7 +1,7 @@
/* global DocumentTouch */
import $ from 'jquery';
-import sortableConfig from '../../sortable/sortable_config';
+import sortableConfig from 'ee_else_ce/sortable/sortable_config';
export function sortableStart() {
$('.has-tooltip')
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index 17ea4d77795..6e632519d8a 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -80,6 +80,9 @@ const applicationStateMachine = {
installFailed: false,
},
},
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ },
// This is possible in artificial environments for E2E testing
[INSTALLED]: {
target: INSTALLED,
@@ -108,6 +111,9 @@ const applicationStateMachine = {
updateSuccessful: false,
},
},
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ },
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index aaa9f8b759a..58d5b658b17 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -49,6 +49,8 @@ export default {
return this.author.id ? this.author.id : '';
},
authorUrl() {
+ // TODO: when the vue i18n rules are merged need to disable @gitlab/i18n/no-non-i18n-strings
+ // name: 'mailto:' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
return this.author.web_url || `mailto:${this.commit.author_email}`;
},
authorAvatar() {
@@ -80,7 +82,7 @@ export default {
v-html="commit.title_html"
></a>
- <span class="commit-row-message d-block d-sm-none"> &middot; {{ commit.short_id }} </span>
+ <span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
<button
v-if="commit.description_html"
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index 80aec84f574..1dcdb65d5c7 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -1,6 +1,6 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
-import { n__, __ } from '~/locale';
+import { n__, __, sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
@@ -54,11 +54,7 @@ export default {
},
methods: {
commitsText(version) {
- return n__(
- `${version.commits_count} commit,`,
- `${version.commits_count} commits,`,
- version.commits_count,
- );
+ return n__(`%d commit,`, `%d commits,`, version.commits_count);
},
href(version) {
if (this.isBase(version)) {
@@ -76,7 +72,7 @@ export default {
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
- return `version ${version.version_index}`;
+ return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
},
isActive(version) {
if (!version) {
@@ -125,9 +121,9 @@ export default {
<div>
<strong>
{{ versionName(version) }}
- <template v-if="isBase(version)">
- (base)
- </template>
+ <template v-if="isBase(version)">{{
+ s__('DiffsCompareBaseBranch|(base)')
+ }}</template>
</strong>
</div>
<div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index f5876a73eff..63350fafefa 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -151,21 +151,22 @@ export default {
<div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
<span class="file-fork-suggestion-note">
- You're not allowed to <span class="js-file-fork-suggestion-section-action">edit</span> files
- in this project directly. Please fork this project, make your changes there, and submit a
- merge request.
+ {{ sprintf(__("You're not allowed to %{tag_start}edit%{tag_end} files in this project
+ directly. Please fork this project, make your changes there, and submit a merge request."),
+ { tag_start: '<span class="js-file-fork-suggestion-section-action">', tag_end: '</span>' })
+ }}
</span>
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
- >Fork</a
+ >{{ __('Fork') }}</a
>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@click="hideForkMessage"
>
- Cancel
+ {{ __('Cancel') }}
</button>
</div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 7cf3d90d468..e28909b7be3 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -74,7 +74,7 @@ export default {
<button
v-if="discussionsExpanded"
type="button"
- aria-label="Show comments"
+ :aria-label="__('Show comments')"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="toggleDiscussions"
>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 95782b2c88a..1af86a94482 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -30,6 +30,9 @@ export default {
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
+ actualTreeList() {
+ return this.currentTree.tree.filter(entry => !entry.moved);
+ },
},
mounted() {
this.updateViewer(this.viewerType);
@@ -54,9 +57,9 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
- <template v-if="currentTree.tree.length">
+ <template v-if="actualTreeList.length">
<file-row
- v-for="file in currentTree.tree"
+ v-for="file in actualTreeList"
:key="file.key"
:file="file"
:level="0"
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 48aabaf9dcf..507dc363529 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -8,6 +8,7 @@ import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
import service from '../services';
+import router from '../ide_router';
export const redirectToUrl = (self, url) => visitUrl(url);
@@ -234,10 +235,15 @@ export const renameEntry = (
parentPath: newParentPath,
});
});
- }
+ } else {
+ const newPath = parentPath ? `${parentPath}/${name}` : name;
+ const newEntry = state.entries[newPath];
+ commit(types.TOGGLE_FILE_CHANGED, { file: newEntry, changed: true });
- if (!entryPath && !entry.tempFile) {
- dispatch('deleteEntry', path);
+ if (entry.opened) {
+ router.push(`/project${newEntry.url}`);
+ commit(types.TOGGLE_FILE_OPEN, entry.path);
+ }
}
dispatch('triggerFilesChange');
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index dc40a1fa6a2..7627b6e03af 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -73,7 +73,9 @@ export const getFileData = (
.getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/')))
.then(({ data, headers }) => {
const normalizedHeaders = normalizeHeaders(headers);
- setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
+ let title = normalizedHeaders['PAGE-TITLE'];
+ title = file.prevPath ? title.replace(file.prevPath, file.path) : title;
+ setPageTitle(decodeURI(title));
if (data) commit(types.SET_FILE_DATA, { data, file });
if (openFile) commit(types.TOGGLE_FILE_OPEN, path);
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index ae42b87c9a7..ec4c2fdcde2 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -216,15 +216,16 @@ export default {
Vue.set(state.entries, newPath, {
...oldEntry,
id: newPath,
- key: `${newPath}-${oldEntry.type}-${oldEntry.id}`,
+ key: `${newPath}-${oldEntry.type}-${oldEntry.path}`,
path: newPath,
name: entryPath ? oldEntry.name : name,
tempFile: true,
prevPath: oldEntry.tempFile ? null : oldEntry.path,
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
tree: [],
- parentPath,
raw: '',
+ opened: false,
+ parentPath,
});
oldEntry.moved = true;
@@ -241,10 +242,6 @@ export default {
state.changedFiles = state.changedFiles.concat(newEntry);
}
- if (state.entries[newPath].opened) {
- state.openFiles.push(state.entries[newPath]);
- }
-
if (oldEntry.tempFile) {
const filterMethod = f => f.path !== oldEntry.path;
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 4e7a8765abe..fb132c1afc1 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -147,9 +147,9 @@ export const createCommitPayload = ({
commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: commitActionForFile(f),
- file_path: f.path,
+ file_path: f.moved ? f.movedPath : f.path,
previous_path: f.prevPath === '' ? undefined : f.prevPath,
- content: f.content || undefined,
+ content: f.prevPath ? null : f.content || undefined,
encoding: f.base64 ? 'base64' : 'text',
last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
})),
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index d895eca7af0..ae579fef25f 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -3,7 +3,7 @@ import { commitItemIconMap } from './constants';
export const getCommitIconMap = file => {
if (file.deleted) {
return commitItemIconMap.deleted;
- } else if (file.tempFile) {
+ } else if (file.tempFile && !file.prevPath) {
return commitItemIconMap.addition;
}
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
new file mode 100644
index 00000000000..e16ddbfef7e
--- /dev/null
+++ b/app/assets/javascripts/manual_ordering.js
@@ -0,0 +1,58 @@
+import Sortable from 'sortablejs';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import {
+ getBoardSortableDefaultOptions,
+ sortableStart,
+} from '~/boards/mixins/sortable_default_options';
+import axios from '~/lib/utils/axios_utils';
+
+const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
+ axios
+ .put(`${url}/reorder`, {
+ move_before_id,
+ move_after_id,
+ group_full_path: issueList.dataset.groupFullPath,
+ })
+ .catch(() => {
+ createFlash(s__("ManualOrdering|Couldn't save the order of the issues"));
+ });
+
+const initManualOrdering = () => {
+ const issueList = document.querySelector('.manual-ordering');
+
+ if (!issueList || !(gon.features && gon.features.manualSorting)) {
+ return;
+ }
+
+ Sortable.create(
+ issueList,
+ getBoardSortableDefaultOptions({
+ scroll: true,
+ dataIdAttr: 'data-id',
+ fallbackOnBody: false,
+ group: {
+ name: 'issues',
+ },
+ draggable: 'li.issue',
+ onStart: () => {
+ sortableStart();
+ },
+ onUpdate: event => {
+ const el = event.item;
+
+ const url = el.getAttribute('url');
+
+ const prev = el.previousElementSibling;
+ const next = el.nextElementSibling;
+
+ const beforeId = prev && parseInt(prev.dataset.id, 10);
+ const afterId = next && parseInt(next.dataset.id, 10);
+
+ updateIssue(url, issueList, { move_after_id: afterId, move_before_id: beforeId });
+ },
+ }),
+ );
+};
+
+export default initManualOrdering;
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 0a652329dfe..973fa346a4f 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -234,7 +234,7 @@ export default {
</script>
<template>
- <div v-if="!showEmptyState" class="prometheus-graphs">
+ <div class="prometheus-graphs">
<div class="gl-p-3 border-bottom bg-gray-light d-flex justify-content-between">
<div
v-if="environmentsEndpoint"
@@ -253,11 +253,12 @@ export default {
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
+ :href="environment.metrics_path"
>{{ environment.name }}</gl-dropdown-item
>
</gl-dropdown>
</div>
- <div class="d-flex align-items-center prepend-left-8">
+ <div v-if="!showEmptyState" class="d-flex align-items-center prepend-left-8">
<strong>{{ s__('Metrics|Show last') }}</strong>
<gl-dropdown
class="prepend-left-10 js-time-window-dropdown"
@@ -276,7 +277,7 @@ export default {
</div>
</div>
<div class="d-flex">
- <div v-if="isEE && canAddMetrics">
+ <div v-if="isEE && canAddMetrics && !showEmptyState">
<gl-button
v-gl-modal-directive="$options.addMetric.modalId"
class="js-add-metric-button text-success border-success"
@@ -317,40 +318,42 @@ export default {
</gl-button>
</div>
</div>
- <graph-group
- v-for="(groupData, index) in groupsWithData"
- :key="index"
- :name="groupData.group"
- :show-panels="showPanels"
- >
- <monitor-area-chart
- v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
- :key="graphIndex"
- :graph-data="graphData"
- :deployment-data="deploymentData"
- :thresholds="getGraphAlertValues(graphData.queries)"
- :container-width="elWidth"
- group-id="monitor-area-chart"
+ <div v-if="!showEmptyState">
+ <graph-group
+ v-for="(groupData, index) in groupsWithData"
+ :key="index"
+ :name="groupData.group"
+ :show-panels="showPanels"
>
- <alert-widget
- v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData"
- :alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.queries"
- :alerts-to-manage="getGraphAlerts(graphData.queries)"
- @setAlerts="setAlerts"
- />
- </monitor-area-chart>
- </graph-group>
+ <monitor-area-chart
+ v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
+ :key="graphIndex"
+ :graph-data="graphData"
+ :deployment-data="deploymentData"
+ :thresholds="getGraphAlertValues(graphData.queries)"
+ :container-width="elWidth"
+ group-id="monitor-area-chart"
+ >
+ <alert-widget
+ v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData"
+ :alerts-endpoint="alertsEndpoint"
+ :relevant-queries="graphData.queries"
+ :alerts-to-manage="getGraphAlerts(graphData.queries)"
+ @setAlerts="setAlerts"
+ />
+ </monitor-area-chart>
+ </graph-group>
+ </div>
+ <empty-state
+ v-else
+ :selected-state="emptyState"
+ :documentation-path="documentationPath"
+ :settings-path="settingsPath"
+ :clusters-path="clustersPath"
+ :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
+ :empty-loading-svg-path="emptyLoadingSvgPath"
+ :empty-no-data-svg-path="emptyNoDataSvgPath"
+ :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
+ />
</div>
- <empty-state
- v-else
- :selected-state="emptyState"
- :documentation-path="documentationPath"
- :settings-path="settingsPath"
- :clusters-path="clustersPath"
- :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
- :empty-loading-svg-path="emptyLoadingSvgPath"
- :empty-no-data-svg-path="emptyNoDataSvgPath"
- :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
- />
</template>
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index 9055738f86e..2ffeed8a584 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
+import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
});
projectSelect();
+ initManualOrdering();
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 35d4b034654..23fb5656008 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -12,4 +13,5 @@ document.addEventListener('DOMContentLoaded', () => {
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
projectSelect();
+ initManualOrdering();
});
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index c34aff02111..c73ebb31eb3 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -7,6 +7,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation();
new UsersSelect();
+ initManualOrdering();
});
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index d67d88c4dba..c8819cf35cf 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -1,8 +1,8 @@
import Visibility from 'visibilityjs';
+import PipelineStore from 'ee_else_ce/pipelines/stores/pipeline_store';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
-import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
export default class pipelinesMediator {
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 0d4d431855c..67963dc1923 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -36,7 +36,7 @@ export default {
to: `/tree/${this.ref}${path}`,
});
},
- [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}` }],
+ [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}/` }],
);
},
},
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 891e3fe9d16..1e66ccbfa29 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -131,7 +131,9 @@ export default {
v-for="entry in val"
:id="entry.id"
:key="`${entry.flatPath}-${entry.id}`"
+ :project-path="projectPath"
:current-path="path"
+ :name="entry.name"
:path="entry.flatPath"
:type="entry.type"
:url="entry.webUrl"
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 4519f82fc93..c31e7fa71a2 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,12 +1,30 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlLink, GlSkeletonLoading } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { getIconName } from '../../utils/icon';
import getRefMixin from '../../mixins/get_ref';
+import getCommit from '../../queries/getCommit.query.graphql';
export default {
components: {
GlBadge,
+ GlLink,
+ GlSkeletonLoading,
+ TimeagoTooltip,
+ },
+ apollo: {
+ commit: {
+ query: getCommit,
+ variables() {
+ return {
+ fileName: this.name,
+ type: this.type,
+ path: this.currentPath,
+ projectPath: this.projectPath,
+ };
+ },
+ },
},
mixins: [getRefMixin],
props: {
@@ -14,10 +32,18 @@ export default {
type: String,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
currentPath: {
type: String,
required: true,
},
+ name: {
+ type: String,
+ required: true,
+ },
path: {
type: String,
required: true,
@@ -37,6 +63,11 @@ export default {
default: null,
},
},
+ data() {
+ return {
+ commit: null,
+ };
+ },
computed: {
routerLinkTo() {
return this.isFolder ? { path: `/tree/${this.ref}/${this.path}` } : null;
@@ -73,7 +104,7 @@ export default {
</script>
<template>
- <tr v-once :class="`file_${id}`" class="tree-item" @click="openRow">
+ <tr :class="`file_${id}`" class="tree-item" @click="openRow">
<td class="tree-item-file-name">
<i :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
<component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
@@ -83,10 +114,18 @@ export default {
LFS
</gl-badge>
<template v-if="isSubmodule">
- @ <a href="#" class="commit-sha">{{ shortSha }}</a>
+ @ <gl-link href="#" class="commit-sha">{{ shortSha }}</gl-link>
</template>
</td>
- <td class="d-none d-sm-table-cell tree-commit"></td>
- <td class="tree-time-ago text-right"></td>
+ <td class="d-none d-sm-table-cell tree-commit">
+ <gl-link v-if="commit" :href="commit.commitPath" class="str-truncated-100 tree-commit-link">
+ {{ commit.message }}
+ </gl-link>
+ <gl-skeleton-loading v-else :lines="1" class="h-auto" />
+ </td>
+ <td class="tree-time-ago text-right">
+ <timeago-tooltip v-if="commit" :time="commit.committedDate" tooltip-placement="bottom" />
+ <gl-skeleton-loading v-else :lines="1" class="ml-auto h-auto w-50" />
+ </td>
</tr>
</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index ef147ec15cb..6cb253c8169 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
+import { fetchLogsTree } from './log_tree';
Vue.use(VueApollo);
@@ -13,7 +14,21 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
});
const defaultClient = createDefaultClient(
- {},
+ {
+ Query: {
+ commit(_, { path, fileName, type }) {
+ return new Promise(resolve => {
+ fetchLogsTree(defaultClient, path, '0', {
+ resolve,
+ entry: {
+ name: fileName,
+ type,
+ },
+ });
+ });
+ },
+ },
+ },
{
cacheConfig: {
fragmentMatcher,
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index d9216e88676..6280977b05b 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -16,6 +16,7 @@ export default function setupVueRepositoryList() {
projectPath,
projectShortPath,
ref,
+ commits: [],
},
});
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
new file mode 100644
index 00000000000..2c19aca2397
--- /dev/null
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -0,0 +1,64 @@
+import axios from '~/lib/utils/axios_utils';
+import getCommits from './queries/getCommits.query.graphql';
+import getProjectPath from './queries/getProjectPath.query.graphql';
+import getRef from './queries/getRef.query.graphql';
+
+let fetchpromise;
+let resolvers = [];
+
+export function normalizeData(data) {
+ return data.map(d => ({
+ sha: d.commit.id,
+ message: d.commit.message,
+ committedDate: d.commit.committed_date,
+ commitPath: d.commit_path,
+ fileName: d.file_name,
+ type: d.type,
+ __typename: 'LogTreeCommit',
+ }));
+}
+
+export function resolveCommit(commits, { resolve, entry }) {
+ const commit = commits.find(c => c.fileName === entry.name && c.type === entry.type);
+
+ if (commit) {
+ resolve(commit);
+ }
+}
+
+export function fetchLogsTree(client, path, offset, resolver = null) {
+ if (resolver) {
+ resolvers.push(resolver);
+ }
+
+ if (fetchpromise) return fetchpromise;
+
+ const { projectPath } = client.readQuery({ query: getProjectPath });
+ const { ref } = client.readQuery({ query: getRef });
+
+ fetchpromise = axios
+ .get(`${gon.gitlab_url}/${projectPath}/refs/${ref}/logs_tree${path ? `/${path}` : ''}`, {
+ params: { format: 'json', offset },
+ })
+ .then(({ data, headers }) => {
+ const headerLogsOffset = headers['more-logs-offset'];
+ const { commits } = client.readQuery({ query: getCommits });
+ const newCommitData = [...commits, ...normalizeData(data)];
+ client.writeQuery({
+ query: getCommits,
+ data: { commits: newCommitData },
+ });
+
+ resolvers.forEach(r => resolveCommit(newCommitData, r));
+
+ fetchpromise = null;
+
+ if (headerLogsOffset) {
+ fetchLogsTree(client, path, headerLogsOffset);
+ } else {
+ resolvers = [];
+ }
+ });
+
+ return fetchpromise;
+}
diff --git a/app/assets/javascripts/repository/queries/getCommit.query.graphql b/app/assets/javascripts/repository/queries/getCommit.query.graphql
new file mode 100644
index 00000000000..e2a2d831e47
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getCommit.query.graphql
@@ -0,0 +1,10 @@
+query getCommit($fileName: String!, $type: String!, $path: String!) {
+ commit(path: $path, fileName: $fileName, type: $type) @client {
+ sha
+ message
+ committedDate
+ commitPath
+ fileName
+ type
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/getCommits.query.graphql b/app/assets/javascripts/repository/queries/getCommits.query.graphql
new file mode 100644
index 00000000000..df9e67cc440
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getCommits.query.graphql
@@ -0,0 +1,10 @@
+query getCommits {
+ commits @client {
+ sha
+ message
+ committedDate
+ commitPath
+ fileName
+ type
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql
index ef924fde556..4c24fc4087f 100644
--- a/app/assets/javascripts/repository/queries/getFiles.query.graphql
+++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql
@@ -1,5 +1,6 @@
fragment TreeEntry on Entry {
id
+ name
flatPath
type
}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index 13955529cab..bc263bc36e4 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -70,9 +70,6 @@ export default {
:title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
class="compare-meter"
- data-toggle="tooltip"
- data-placement="top"
- role="timeRemainingDisplay"
>
<gl-progress-bar :value="timeRemainingPercent" :variant="progressBarVariant" />
<div class="compare-display-container">
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 22ac8df9699..643fe6c00b6 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,7 +1,7 @@
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import Service from './services/sidebar_service';
-import Store from './stores/sidebar_store';
+import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import { __ } from '~/locale';
export default class SidebarMediator {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index dcbb23684d1..6a0127eb51c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -594,18 +594,18 @@
padding: 16px 0;
small {
- color: $gray-darkest;
+ color: $gray-700;
}
}
.edited-text {
- color: $gray-darkest;
+ color: $gray-700;
display: block;
margin: 16px 0 0;
font-size: 85%;
.author-link {
- color: $gray-darkest;
+ color: $gray-700;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 48289c8f381..8359a60ec9f 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,4 +1,18 @@
.issues-list {
+ &.manual-ordering {
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ padding: $gl-padding-8;
+
+ .issue {
+ background-color: $white-light;
+ margin-bottom: $gl-padding-8;
+ border-radius: $border-radius-default;
+ border: 1px solid $gray-100;
+ box-shadow: 0 1px 2px $issue-boards-card-shadow;
+ }
+ }
+
.issue {
padding: 10px 0 10px $gl-padding;
position: relative;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 5cacd42bf0d..824edb2869f 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -628,7 +628,7 @@ $note-form-margin-left: 72px;
.note-headline-meta {
.system-note-separator {
- color: $gl-text-color-disabled;
+ color: $gray-700;
}
.note-timestamp {
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index 54c0510497f..d5830f6648c 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -6,7 +6,7 @@ module ContinueParams
def continue_params
continue_params = params[:continue]
- return unless continue_params
+ return {} unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
continue_params[:to] = safe_redirect_path(continue_params[:to])
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
index 6785e6972d0..fa3716502a0 100644
--- a/app/controllers/concerns/internal_redirect.rb
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -5,8 +5,8 @@ module InternalRedirect
def safe_redirect_path(path)
return unless path
- # Verify that the string starts with a `/` but not a double `/`.
- return unless path =~ %r{^/\w.*$}
+ # Verify that the string starts with a `/` and a known route character.
+ return unless path =~ %r{^/[-\w].*$}
uri = URI(path)
# Ignore anything path of the redirect except for the path, querystring and,
diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
index 426f224d26b..f47ead2f0da 100644
--- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
+++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
@@ -14,6 +14,10 @@ module RequiresWhitelistedMonitoringClient
end
def client_ip_whitelisted?
+ # Always allow developers to access http://localhost:3000/-/metrics for
+ # debugging purposes
+ return true if Rails.env.development? && request.local?
+
ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.client_ip) }
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index e936d771502..316da8f129d 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -7,6 +7,10 @@ class GroupsController < Groups::ApplicationController
include PreviewMarkdown
include RecordUserLastActivity
+ before_action do
+ push_frontend_feature_flag(:manual_sorting)
+ end
+
respond_to :html
prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 7a1700a206a..ac1c4bc7fd3 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -46,18 +46,14 @@ class Projects::ForksController < Projects::ApplicationController
@forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
- if @forked_project.saved? && @forked_project.forked?
- if @forked_project.import_in_progress?
- redirect_to project_import_path(@forked_project, continue: continue_params)
- else
- if continue_params
- redirect_to continue_params[:to], notice: continue_params[:notice]
- else
- redirect_to project_path(@forked_project), notice: "The project '#{@forked_project.name}' was successfully forked."
- end
- end
- else
+ if !@forked_project.saved? || !@forked_project.forked?
render :error
+ elsif @forked_project.import_in_progress?
+ redirect_to project_import_path(@forked_project, continue: continue_params)
+ elsif continue_params[:to]
+ redirect_to continue_params[:to], notice: continue_params[:notice]
+ else
+ redirect_to project_path(@forked_project), notice: "The project '#{@forked_project.name}' was successfully forked."
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index afbf9fd7720..da32ab9e2e0 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -23,7 +23,7 @@ class Projects::ImportsController < Projects::ApplicationController
def show
if @project.import_finished?
- if continue_params&.key?(:to)
+ if continue_params[:to]
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to project_path(@project), notice: finished_notice
@@ -31,11 +31,7 @@ class Projects::ImportsController < Projects::ApplicationController
elsif @project.import_failed?
redirect_to new_project_import_path(@project)
else
- if continue_params && continue_params[:notice_now]
- flash.now[:notice] = continue_params[:notice_now]
- end
-
- # Render
+ flash.now[:notice] = continue_params[:notice_now]
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b16f3dd9d82..f221f0363d3 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,6 +10,10 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
include RecordUserLastActivity
+ before_action do
+ push_frontend_feature_flag(:manual_sorting)
+ end
+
def issue_except_actions
%i[index calendar new create bulk_update import_csv]
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index d7c0039b234..02ff6e872c9 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -103,7 +103,7 @@ class Projects::JobsController < Projects::ApplicationController
@build.cancel
- if continue_params
+ if continue_params[:to]
redirect_to continue_params[:to]
else
redirect_to builds_project_pipeline_path(@project, @build.pipeline.id)
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index b3447812ef2..b4ca9074ca9 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -55,6 +55,7 @@ class Projects::RefsController < Projects::ApplicationController
format.html { render_404 }
format.json do
response.headers["More-Logs-Url"] = @more_log_url if summary.more?
+ response.headers["More-Logs-Offset"] = summary.next_offset if summary.more?
render json: @logs
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 07b38371ab9..b2b151bbcf0 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -3,6 +3,7 @@
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
+ include RecaptchaExperimentHelper
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
@@ -15,13 +16,6 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
- # To avoid duplicate form fields on the login page, the registration form
- # names fields using `new_user`, but Devise still wants the params in
- # `user`.
- if params["new_#{resource_name}"].present? && params[resource_name].blank?
- params[resource_name] = params.delete(:"new_#{resource_name}")
- end
-
accept_pending_invitations
super do |new_user|
@@ -74,19 +68,35 @@ class RegistrationsController < Devise::RegistrationsController
end
def after_sign_up_path_for(user)
- Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
+ Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?))
user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(resource)
- Gitlab::AppLogger.info("User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:false")
+ Gitlab::AppLogger.info(user_created_message)
users_almost_there_path
end
private
+ def user_created_message(confirmed: false)
+ "User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}"
+ end
+
+ def ensure_correct_params!
+ # To avoid duplicate form fields on the login page, the registration form
+ # names fields using `new_user`, but Devise still wants the params in
+ # `user`.
+ if params["new_#{resource_name}"].present? && params[resource_name].blank?
+ params[resource_name] = params.delete(:"new_#{resource_name}")
+ end
+ end
+
def check_captcha
- return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true)
+ ensure_correct_params!
+
+ return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true) # reCAPTCHA on the UI will still display however
+ return unless show_recaptcha_sign_up?
return unless Gitlab::Recaptcha.load_configurations!
return if verify_recaptcha
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 59332c0b100..dfadcfc33b2 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -5,6 +5,7 @@ module IssuesHelper
classes = ["issue"]
classes << "closed" if issue.closed?
classes << "today" if issue.today?
+ classes << "user-can-drag" if @sort == 'relative_position'
classes.join(' ')
end
diff --git a/app/helpers/recaptcha_experiment_helper.rb b/app/helpers/recaptcha_experiment_helper.rb
new file mode 100644
index 00000000000..d2eb9ac54f6
--- /dev/null
+++ b/app/helpers/recaptcha_experiment_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module RecaptchaExperimentHelper
+ def show_recaptcha_sign_up?
+ !!Gitlab::Recaptcha.enabled?
+ end
+end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 0fd8dca70b4..da4584228ce 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -45,7 +45,7 @@ class BroadcastMessage < ApplicationRecord
end
def self.cache_expires_in
- nil
+ 2.weeks
end
def active?
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 3c270c7396a..f9b53b2b70a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -35,6 +35,8 @@ class Namespace < ApplicationRecord
belongs_to :parent, class_name: "Namespace"
has_many :children, class_name: "Namespace", foreign_key: :parent_id
has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
+ has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb
new file mode 100644
index 00000000000..43afd0b954c
--- /dev/null
+++ b/app/models/namespace/aggregation_schedule.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Namespace::AggregationSchedule < ApplicationRecord
+ self.primary_key = :namespace_id
+
+ belongs_to :namespace
+end
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
new file mode 100644
index 00000000000..de28eb6b37f
--- /dev/null
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Namespace::RootStorageStatistics < ApplicationRecord
+ self.primary_key = :namespace_id
+
+ belongs_to :namespace
+ has_one :route, through: :namespace
+
+ delegate :all_projects, to: :namespace
+end
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
index 3413a9e4612..58f795e639e 100644
--- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -2,6 +2,14 @@
module PagesDomains
class ObtainLetsEncryptCertificateService
+ # time for processing validation requests for acme challenges
+ # 5-15 seconds is usually enough
+ CHALLENGE_PROCESSING_DELAY = 1.minute.freeze
+
+ # time LetsEncrypt ACME server needs to generate the certificate
+ # no particular SLA, usually takes 10-15 seconds
+ CERTIFICATE_PROCESSING_DELAY = 1.minute.freeze
+
attr_reader :pages_domain
def initialize(pages_domain)
@@ -14,6 +22,7 @@ module PagesDomains
unless acme_order
::PagesDomains::CreateAcmeOrderService.new(pages_domain).execute
+ PagesDomainSslRenewalWorker.perform_in(CHALLENGE_PROCESSING_DELAY, pages_domain.id)
return
end
@@ -23,6 +32,7 @@ module PagesDomains
case api_order.status
when 'ready'
api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain)
+ PagesDomainSslRenewalWorker.perform_in(CERTIFICATE_PROCESSING_DELAY, pages_domain.id)
when 'valid'
save_certificate(acme_order.private_key, api_order)
acme_order.destroy!
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb
index a2f36d2bd1b..a25c985585b 100644
--- a/app/services/projects/propagate_service_template.rb
+++ b/app/services/projects/propagate_service_template.rb
@@ -24,7 +24,7 @@ module Projects
def propagate_projects_with_template
loop do
- batch = project_ids_batch
+ batch = Project.uncached { project_ids_batch }
bulk_create_from_template(batch) unless batch.empty?
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index a34fc15acb1..d24e46b2815 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -7,7 +7,10 @@
= f.check_box :recaptcha_enabled, class: 'form-check-input'
= f.label :recaptcha_enabled, class: 'form-check-label' do
Enable reCAPTCHA
- %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts
+ - recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
+ - recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url }
+ %span.form-text.text-muted#recaptcha_help_block
+ = _('Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe }
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index eae3ee6339f..034273558bb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -33,7 +33,7 @@
= accept_terms_label.html_safe
= render_if_exists 'devise/shared/email_opted_in', f: f
%div
- - if Gitlab::Recaptcha.enabled?
+ - if show_recaptcha_sign_up?
= recaptcha_tags
.submit-container
= f.submit _("Register"), class: "btn-register btn qa-new-user-register-button"
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 6f7713124ac..7d539c9d749 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,6 +1,6 @@
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
-%ul.content-list.issues-list.issuable-list
+%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
= render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
= render empty_state_path
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 987a5d4f13f..a21dcabb485 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,6 +1,6 @@
- if @issues.to_a.any?
.card.card-small.card-without-border
- %ul.content-list.issues-list.issuable-list
+ %ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
- else
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index 1dd97bc4ed1..403e001bfe8 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -1,6 +1,7 @@
- sort_value = @sort
- sort_title = issuable_sort_option_title(sort_value)
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
+- manual_sorting = viewing_issues && controller.controller_name != 'dashboard' && Feature.enabled?(:manual_sorting)
.dropdown.inline.prepend-left-10.issue-sort-dropdown
.btn-group{ role: 'group' }
@@ -17,6 +18,6 @@
= sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
= sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
= sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
- = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues && Feature.enabled?(:manual_sorting)
+ = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if manual_sorting
= render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
= issuable_sort_direction_button(sort_value)
diff --git a/changelogs/unreleased/51952-forking-via-webide.yml b/changelogs/unreleased/51952-forking-via-webide.yml
new file mode 100644
index 00000000000..4497c6b6ca4
--- /dev/null
+++ b/changelogs/unreleased/51952-forking-via-webide.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve "500 error when forking via the web IDE button"
+merge_request: 29909
+author:
+type: fixed
diff --git a/changelogs/unreleased/58802-rename-webide.yml b/changelogs/unreleased/58802-rename-webide.yml
new file mode 100644
index 00000000000..40471d967ce
--- /dev/null
+++ b/changelogs/unreleased/58802-rename-webide.yml
@@ -0,0 +1,5 @@
+---
+title: Re-name files in Web IDE in a more natural way
+merge_request: 29948
+author:
+type: changed
diff --git a/changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml b/changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml
deleted file mode 100644
index 14a8da95ed9..00000000000
--- a/changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix missing API notification flags for Microsoft Teams
-merge_request: 29824
-author: Seiji Suenaga
-type: fixed
diff --git a/changelogs/unreleased/62938-wcag-aa-edited-text-color.yml b/changelogs/unreleased/62938-wcag-aa-edited-text-color.yml
new file mode 100644
index 00000000000..6652e495869
--- /dev/null
+++ b/changelogs/unreleased/62938-wcag-aa-edited-text-color.yml
@@ -0,0 +1,5 @@
+---
+title: Use darker gray color for system note metadata and edited text
+merge_request: 30054
+author:
+type: other
diff --git a/changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml b/changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml
deleted file mode 100644
index b5715902630..00000000000
--- a/changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include the GitLab version in the cache key for Gitlab::JsonCache
-merge_request: 29938
-author:
-type: fixed
diff --git a/changelogs/unreleased/always-allow-prometheus-access-in-dev.yml b/changelogs/unreleased/always-allow-prometheus-access-in-dev.yml
new file mode 100644
index 00000000000..acd944ea684
--- /dev/null
+++ b/changelogs/unreleased/always-allow-prometheus-access-in-dev.yml
@@ -0,0 +1,5 @@
+---
+title: Always allow access to health endpoints from localhost in dev
+merge_request: 29930
+author:
+type: other
diff --git a/changelogs/unreleased/always-display-environment-selector.yml b/changelogs/unreleased/always-display-environment-selector.yml
new file mode 100644
index 00000000000..7a55e8f3e5d
--- /dev/null
+++ b/changelogs/unreleased/always-display-environment-selector.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken environment selector and always display it on monitoring dashboard
+merge_request: 29705
+author:
+type: fixed
diff --git a/changelogs/unreleased/bug-63162-duplicate_path_in_links.yml b/changelogs/unreleased/bug-63162-duplicate_path_in_links.yml
deleted file mode 100644
index d3f246492fb..00000000000
--- a/changelogs/unreleased/bug-63162-duplicate_path_in_links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed 'diff version changes' link not working
-merge_request: 29825
-author:
-type: fixed
diff --git a/changelogs/unreleased/fe-issue-reorder.yml b/changelogs/unreleased/fe-issue-reorder.yml
new file mode 100644
index 00000000000..aca334b6149
--- /dev/null
+++ b/changelogs/unreleased/fe-issue-reorder.yml
@@ -0,0 +1,5 @@
+---
+title: Bring Manual Ordering on Issue List
+merge_request: 29410
+author:
+type: added
diff --git a/changelogs/unreleased/fix-jupyter-git-v3.yml b/changelogs/unreleased/fix-jupyter-git-v3.yml
new file mode 100644
index 00000000000..8aaaaf249fb
--- /dev/null
+++ b/changelogs/unreleased/fix-jupyter-git-v3.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Jupyter-Git integration
+merge_request: 30020
+author: Amit Rathi
+type: fixed
diff --git a/changelogs/unreleased/fix-labels-in-hooks.yml b/changelogs/unreleased/fix-labels-in-hooks.yml
deleted file mode 100644
index c0904a860c5..00000000000
--- a/changelogs/unreleased/fix-labels-in-hooks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix label serialization in issue and note hooks
-merge_request: 29850
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-notes-emails-with-group-settings.yml b/changelogs/unreleased/fix-notes-emails-with-group-settings.yml
deleted file mode 100644
index 77dae8418a8..00000000000
--- a/changelogs/unreleased/fix-notes-emails-with-group-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix comment emails not respecting group-level notification email
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-cache-negative-entries-find-commit.yml b/changelogs/unreleased/sh-cache-negative-entries-find-commit.yml
new file mode 100644
index 00000000000..98eb13ee620
--- /dev/null
+++ b/changelogs/unreleased/sh-cache-negative-entries-find-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Allow caching of negative FindCommit matches
+merge_request: 29952
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-omit-issues-links-on-poll.yml b/changelogs/unreleased/sh-omit-issues-links-on-poll.yml
deleted file mode 100644
index 21e51d3534f..00000000000
--- a/changelogs/unreleased/sh-omit-issues-links-on-poll.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Omit issues links in merge request entity API response
-merge_request: 29917
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-quiet-backup-secrets-log.yml b/changelogs/unreleased/sh-quiet-backup-secrets-log.yml
deleted file mode 100644
index cf3e90c0cb1..00000000000
--- a/changelogs/unreleased/sh-quiet-backup-secrets-log.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Silence backup warnings when CRON=1 in use
-merge_request: 30033
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-recover-ee-schema-backport-migration-failure.yml b/changelogs/unreleased/sh-recover-ee-schema-backport-migration-failure.yml
deleted file mode 100644
index 1695e2827eb..00000000000
--- a/changelogs/unreleased/sh-recover-ee-schema-backport-migration-failure.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent EE backport migrations from running if CE is not migrated
-merge_request: 30002
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-service-template-bug.yml b/changelogs/unreleased/sh-service-template-bug.yml
new file mode 100644
index 00000000000..1ea5ac84f26
--- /dev/null
+++ b/changelogs/unreleased/sh-service-template-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Disable Rails SQL query cache when applying service templates
+merge_request: 30060
+author:
+type: fixed
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4b0bb86e42a..9e74a67b73f 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -368,7 +368,7 @@ Settings.cron_jobs['pages_domain_removal_cron_worker']['cron'] ||= '47 0 * * *'
Settings.cron_jobs['pages_domain_removal_cron_worker']['job_class'] = 'PagesDomainRemovalCronWorker'
Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker']['cron'] ||= '*/5 * * * *'
+Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker']['cron'] ||= '*/10 * * * *'
Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker']['job_class'] = 'PagesDomainSslRenewalCronWorker'
Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
diff --git a/db/migrate/20190531153110_create_namespace_root_storage_statistics.rb b/db/migrate/20190531153110_create_namespace_root_storage_statistics.rb
new file mode 100644
index 00000000000..702560d05cc
--- /dev/null
+++ b/db/migrate/20190531153110_create_namespace_root_storage_statistics.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateNamespaceRootStorageStatistics < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ create_table :namespace_root_storage_statistics, id: false, primary_key: :namespace_id do |t|
+ t.integer :namespace_id, null: false, primary_key: true
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.bigint :repository_size, null: false, default: 0
+ t.bigint :lfs_objects_size, null: false, default: 0
+ t.bigint :wiki_size, null: false, default: 0
+ t.bigint :build_artifacts_size, null: false, default: 0
+ t.bigint :storage_size, null: false, default: 0
+ t.bigint :packages_size, null: false, default: 0
+
+ t.index :namespace_id, unique: true
+ t.foreign_key :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20190605184422_create_namespace_aggregation_schedules.rb b/db/migrate/20190605184422_create_namespace_aggregation_schedules.rb
new file mode 100644
index 00000000000..5e8cb616cc1
--- /dev/null
+++ b/db/migrate/20190605184422_create_namespace_aggregation_schedules.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateNamespaceAggregationSchedules < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ create_table :namespace_aggregation_schedules, id: false, primary_key: :namespace_id do |t|
+ t.integer :namespace_id, null: false, primary_key: true
+
+ t.index :namespace_id, unique: true
+ t.foreign_key :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 75f72c3b447..054dbc7201f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2051,6 +2051,21 @@ ActiveRecord::Schema.define(version: 20190625184066) do
t.index ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
end
+ create_table "namespace_aggregation_schedules", primary_key: "namespace_id", id: :integer, default: nil, force: :cascade do |t|
+ t.index ["namespace_id"], name: "index_namespace_aggregation_schedules_on_namespace_id", unique: true, using: :btree
+ end
+
+ create_table "namespace_root_storage_statistics", primary_key: "namespace_id", id: :integer, default: nil, force: :cascade do |t|
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "repository_size", default: 0, null: false
+ t.bigint "lfs_objects_size", default: 0, null: false
+ t.bigint "wiki_size", default: 0, null: false
+ t.bigint "build_artifacts_size", default: 0, null: false
+ t.bigint "storage_size", default: 0, null: false
+ t.bigint "packages_size", default: 0, null: false
+ t.index ["namespace_id"], name: "index_namespace_root_storage_statistics_on_namespace_id", unique: true, using: :btree
+ end
+
create_table "namespace_statistics", id: :serial, force: :cascade do |t|
t.integer "namespace_id", null: false
t.integer "shared_runners_seconds", default: 0, null: false
@@ -3753,6 +3768,8 @@ ActiveRecord::Schema.define(version: 20190625184066) do
add_foreign_key "merge_trains", "users", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
+ add_foreign_key "namespace_aggregation_schedules", "namespaces", on_delete: :cascade
+ add_foreign_key "namespace_root_storage_statistics", "namespaces", on_delete: :cascade
add_foreign_key "namespace_statistics", "namespaces", on_delete: :cascade
add_foreign_key "namespaces", "namespaces", column: "custom_project_templates_group_id", name: "fk_e7a0b20a6b", on_delete: :nullify
add_foreign_key "namespaces", "plans", name: "fk_fdd12e5b80", on_delete: :nullify
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 0e655e49922..b1126881440 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -158,12 +158,11 @@ If you enable Monitoring, it must be enabled on **all** GitLab servers.
sidekiq['listen_address'] = "0.0.0.0"
unicorn['listen'] = '0.0.0.0'
- # Add the monitoring node's IP address to the monitoring whitelist and allow it to scrape the NGINX metrics
- # Replace placeholder
- # monitoring.gitlab.example.com
- # with the addresses gathered for the monitoring node
- gitlab_rails['monitoring_whitelist'] = ['monitoring.gitlab.example.com']
- nginx['status']['options']['allow'] = ['monitoring.gitlab.example.com']
+ # Add the monitoring node's IP address to the monitoring whitelist and allow it to
+ # scrape the NGINX metrics. Replace placeholder `monitoring.gitlab.example.com` with
+ # the address and/or subnets gathered from the monitoring node(s).
+ gitlab_rails['monitoring_whitelist'] = ['monitoring.gitlab.example.com', '127.0.0.0/8']
+ nginx['status']['options']['allow'] = ['monitoring.gitlab.example.com', '127.0.0.0/8']
```
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
diff --git a/doc/administration/high_availability/pgbouncer.md b/doc/administration/high_availability/pgbouncer.md
index 762179cf756..053dae25823 100644
--- a/doc/administration/high_availability/pgbouncer.md
+++ b/doc/administration/high_availability/pgbouncer.md
@@ -62,6 +62,33 @@ See our [HA documentation for PostgreSQL](database.md) for information on runnin
1. At this point, your instance should connect to the database through pgbouncer. If you are having issues, see the [Troubleshooting](#troubleshooting) section
+## Enable Monitoring
+
+> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3786) in GitLab 12.0.
+
+ If you enable Monitoring, it must be enabled on **all** pgbouncer servers.
+
+ 1. Create/edit `/etc/gitlab/gitlab.rb` and add the following configuration:
+
+ ```ruby
+ # Enable service discovery for Prometheus
+ consul['enable'] = true
+ consul['monitoring_service_discovery'] = true
+
+ # Replace placeholders
+ # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
+ # with the addresses of the Consul server nodes
+ consul['configuration'] = {
+ retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z),
+ }
+
+ # Set the network addresses that the exporters will listen on
+ node_exporter['listen_address'] = '0.0.0.0:9100'
+ pgbouncer_exporter['listen_address'] = '0.0.0.0:9188'
+ ```
+
+ 1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+
### Interacting with pgbouncer
#### Administrative console
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 1743c38eb46..da864a0b3cc 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -63,7 +63,7 @@ Once you're familiar with how GitLab CI/CD works, see the
for all the attributes you can set and use.
NOTE: **Note:**
-GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota).
+GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota-free-only).
## Configuration
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index b4c4bea6447..efdcaf5a6f5 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -205,7 +205,14 @@ An example project using this approach can be found here: <https://gitlab.com/gi
### Use Docker socket binding
-The third approach is to bind-mount `/var/run/docker.sock` into the container so that docker is available in the context of that image.
+The third approach is to bind-mount `/var/run/docker.sock` into the
+container so that Docker is available in the context of that image.
+
+NOTE: **Note:**
+If you bind the Docker socket [when using GitLab Runner 11.11 or
+newer](https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1261),
+you can no longer use `docker:dind` as a service because volume bindings
+are done to the services as well, making these incompatible.
In order to do that, follow the steps:
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 2ed2a905db7..aeddad14995 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -447,7 +447,7 @@ want to validate the abilities for.
Alternatively, we can add a `find_object` method that will load the
object on the mutation. This would allow you to use the
-`authorized_find!` and `authorized_find!` helper methods.
+`authorized_find!` helper method.
When a user is not allowed to perform the action, or an object is not
found, we should raise a
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 23d52a33881..ff6dc16d1a0 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -409,11 +409,20 @@ To indicate the steps of navigation through the UI:
## Images
- Place images in a separate directory named `img/` in the same directory where
- the `.md` document that you're working on is located. Always prepend their
- names with the name of the document that they will be included in. For
- example, if there is a document called `twitter.md`, then a valid image name
- could be `twitter_login_screen.png`.
-- Images should have a specific, non-generic name that will differentiate and describe them properly.
+ the `.md` document that you're working on is located.
+- Images should have a specific, non-generic name that will
+ differentiate and describe them properly.
+- Always add to the end of the file name the GitLab release version
+ number corresponding to the release milestone the image was added to,
+ or corresponding to the release the screenshot was taken from, using the
+ format `image_name_vX_Y.png`.
+ ([Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/61027) in GitLab 12.1.)
+- For example, for a screenshot taken from the pipelines page of
+ GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're
+ adding an illustration that does not include parts of the UI,
+ add the release number corresponding to the release the image
+ was added to. Example, for an MR added to 11.1's milestone,
+ a valid name for an illustration is `devops_diagram_v11_1.png`.
- Keep all file names in lower case.
- Consider using PNG images instead of JPEG.
- Compress all images with <https://tinypng.com/> or similar tool.
@@ -426,7 +435,7 @@ To indicate the steps of navigation through the UI:
Inside the document:
- The Markdown way of using an image inside a document is:
- `![Proper description what the image is about](img/document_image_title.png)`
+ `![Proper description what the image is about](img/document_image_title_vX_Y.png)`
- Always use a proper description for what the image is about. That way, when a
browser fails to show the image, this text will be used as an alternative
description.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index fc9b175bc8a..28ebb6f0f64 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -27,7 +27,7 @@ we need to solve before being able to use Jest for all our needs.
### Differences to Karma
- Jest runs in a Node.js environment, not in a browser. Support for running Jest tests in a browser [is planned](https://gitlab.com/gitlab-org/gitlab-ce/issues/58205).
-- Because Jest runs in a Node.js environment, it uses [jsdom](https://github.com/jsdom/jsdom) by default.
+- Because Jest runs in a Node.js environment, it uses [jsdom](https://github.com/jsdom/jsdom) by default. See also its [limitations](#limitations-of-jsdom) below.
- Jest does not have access to Webpack loaders or aliases.
The aliases used by Jest are defined in its [own config](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/jest.config.js).
- All calls to `setTimeout` and `setInterval` are mocked away. See also [Jest Timer Mocks](https://jestjs.io/docs/en/timer-mocks).
@@ -40,6 +40,17 @@ we need to solve before being able to use Jest for all our needs.
- Unhandled Promise rejections.
- Calls to `console.warn`, including warnings from libraries like Vue.
+### Limitations of jsdom
+
+As mentioned [above](#differences-to-karma), Jest uses jsdom instead of a browser for running tests.
+This comes with a number of limitations, namely:
+
+- [No scrolling support](https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/browser/Window.js#L623-L625)
+- [No element sizes or positions](https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/living/nodes/Element-impl.js#L334-L371)
+- [No layout engine](https://github.com/jsdom/jsdom/issues/1322) in general
+
+See also the issue for [support running Jest tests in browsers](https://gitlab.com/gitlab-org/gitlab-ce/issues/58205).
+
### Debugging Jest tests
Running `yarn jest-debug` will run Jest in debug mode, allowing you to debug/inspect as described in the [Jest docs](https://jestjs.io/docs/en/troubleshooting#tests-are-failing-and-you-don-t-know-why).
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 5768a97e727..84596ff6a2c 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -94,13 +94,11 @@ a group in the **Usage Quotas** page available to the group page settings list.
![Group pipelines quota](img/group_pipelines_quota.png)
-## Extra Shared Runners pipeline minutes quota
+## Extra Shared Runners pipeline minutes quota **[FREE ONLY]**
-NOTE: **Note:**
-Only available on GitLab.com.
-
-You can purchase additional CI minutes so your pipelines will not be blocked after you have
-used all your CI minutes from your main quota.
+If you're using GitLab.com, you can purchase additional CI minutes so your
+pipelines will not be blocked after you have used all your CI minutes from your
+main quota.
In order to purchase additional minutes, you should follow these steps:
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index df413a11af0..26cacbe5545 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -84,9 +84,8 @@ Click on **Register U2F Device** to complete the process.
> **Note:**
Recovery codes are not generated for U2F devices.
-Should you ever lose access to your one time password authenticator, you can use one of the ten provided
-backup codes to login to your account. We suggest copying them, printing them, or downloading them using
-the **Download codes** button for storage in a safe place.
+Immediately after successfully enabling two-factor authentication, you'll be prompted to download a set of set recovery codes. Should you ever lose access to your one time password authenticator, you can use one of them to log in to your account. We suggest copying them, printing them, or downloading them using
+the **Download codes** button for storage in a safe place. If you choose to download them, the file will be called **gitlab-recovery-codes.txt**.
CAUTION: **Caution:**
Each code can be used only once to log in to your account.
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index d585c19fc5c..bc9a11504cd 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -1,5 +1,5 @@
---
-last_updated: 2019-06-04
+last_updated: 2019-06-25
type: concepts, reference, howto
---
@@ -138,9 +138,9 @@ verify your domain's ownership with a TXT record:
> - **Do not** add any special chars after the default Pages
domain. E.g., **do not** point your `subdomain.domain.com` to
`namespace.gitlab.io.` or `namespace.gitlab.io/`.
-> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017
+> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017.
> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2018/07/19/gcp-move-update/#gitlab-pages-and-custom-domains)
- from `52.167.214.135` to `35.185.44.232` in 2018
+ from `52.167.214.135` to `35.185.44.232` in 2018.
### Add your custom domain to GitLab Pages settings
@@ -199,7 +199,7 @@ Certificates are NOT required to add to your custom
highly recommendable.
Let's start with an introduction to the importance of HTTPS.
-Alternatively, jump ahead to [adding certificates to your project](#adding-certificates-to-your-project).
+Alternatively, jump ahead to [adding certificates to your project](#adding-certificates-to-pages).
### Why should I care about HTTPS?
@@ -255,12 +255,12 @@ which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-f
Their certs are valid up to 15 years. See the tutorial on
[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/).
-### Adding certificates to your project
+### Adding certificates to Pages
Regardless the CA you choose, the steps to add your certificate to
your Pages project are the same.
-### What do you need
+#### Requirements
1. A PEM certificate
1. An intermediate certificate
@@ -270,7 +270,7 @@ your Pages project are the same.
These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**.
-### What's what?
+#### Certificate types
- A PEM certificate is the certificate generated by the CA,
which needs to be added to the field **Certificate (PEM)**.
@@ -283,21 +283,32 @@ These fields are found under your **Project**'s **Settings** > **Pages** > **New
- A private key is an encrypted key which validates
your PEM against your domain.
-### Now what?
+#### Add the certificate to your project
-Now that you hopefully understand why you need all
-of this, it's simple:
+Once you've met the requirements:
-- Your PEM certificate needs to be added to the first field
+- Your PEM certificate needs to be added to the first field.
- If your certificate is missing its intermediate, copy
and paste the root certificate (usually available from your CA website)
and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
just jumping a line between them.
-- Copy your private key and paste it in the last field
+- Copy your private key and paste it in the last field.
->**Note:**
+NOTE: **Note:**
**Do not** open certificates or encryption keys in
regular text editors. Always use code editors (such as
Sublime Text, Atom, Dreamweaver, Brackets, etc).
-_Read on about [Creating and Tweaking GitLab CI/CD for GitLab Pages](getting_started_part_four.md)_
+## Force HTTPS for GitLab Pages websites
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28857) in GitLab 10.7.
+
+To make your website's visitors even more secure, you can choose to
+force HTTPS for GitLab Pages. By doing so, all attempts to visit your
+website via HTTP will be automatically redirected to HTTPS via 301.
+
+It works with both GitLab's default domain and with your custom
+domain (as long as you've set a valid certificate for it).
+
+To enable this setting, navigate to your project's **Settings > Pages**
+and tick the checkbox **Force HTTPS (requires valid certificates)**.
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 46c4c755729..8a84744aa2d 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -17,7 +17,7 @@ code_quality:
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:11-8-stable" /code
+ "registry.gitlab.com/gitlab-org/security-products/codequality:12-0-stable" /code
artifacts:
reports:
codequality: gl-code-quality-report.json
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index d21b98d36ea..a80ce462ab0 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -271,26 +271,30 @@ module Gitlab
end
def find_commit(revision)
- if Gitlab::SafeRequestStore.active?
- # We don't use Gitlab::SafeRequestStore.fetch(key) { ... } directly
- # because `revision` can be a branch name, so we can't use it as a key
- # as it could point to another commit later on (happens a lot in
- # tests).
- key = {
- storage: @gitaly_repo.storage_name,
- relative_path: @gitaly_repo.relative_path,
- commit_id: revision
- }
- return Gitlab::SafeRequestStore[key] if Gitlab::SafeRequestStore.exist?(key)
-
- commit = call_find_commit(revision)
- return unless commit
-
- key[:commit_id] = commit.id unless GitalyClient.ref_name_caching_allowed?
+ return call_find_commit(revision) unless Gitlab::SafeRequestStore.active?
+
+ # We don't use Gitlab::SafeRequestStore.fetch(key) { ... } directly
+ # because `revision` can be a branch name, so we can't use it as a key
+ # as it could point to another commit later on (happens a lot in
+ # tests).
+ key = {
+ storage: @gitaly_repo.storage_name,
+ relative_path: @gitaly_repo.relative_path,
+ commit_id: revision
+ }
+ return Gitlab::SafeRequestStore[key] if Gitlab::SafeRequestStore.exist?(key)
+
+ commit = call_find_commit(revision)
+
+ if GitalyClient.ref_name_caching_allowed?
Gitlab::SafeRequestStore[key] = commit
- else
- call_find_commit(revision)
+ return commit
end
+
+ return unless commit
+
+ key[:commit_id] = commit.id
+ Gitlab::SafeRequestStore[key] = commit
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index b367a97105c..ef5caaf5b0e 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -27,12 +27,6 @@ module Gitlab
raise NotImplementedError, "Implement #find_object in #{self.class.name}"
end
- def authorized_find(*args)
- object = find_object(*args)
-
- object if authorized?(object)
- end
-
def authorized_find!(*args)
object = find_object(*args)
authorize!(object)
@@ -48,6 +42,12 @@ module Gitlab
end
def authorized?(object)
+ # Sanity check. We don't want to accidentally allow a developer to authorize
+ # without first adding permissions to authorize against
+ if self.class.required_permissions.empty?
+ raise Gitlab::Graphql::Errors::ArgumentError, "#{self.class.name} has no authorizations"
+ end
+
self.class.required_permissions.all? do |ability|
# The actions could be performed across multiple objects. In which
# case the current user is common, and we could benefit from the
diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb
index d01183d7845..84c6817f3c7 100644
--- a/lib/gitlab/json_cache.rb
+++ b/lib/gitlab/json_cache.rb
@@ -34,7 +34,7 @@ module Gitlab
def read(key, klass = nil)
value = backend.read(cache_key(key))
- value = parse_value(value, klass) if value
+ value = parse_value(value, klass) unless value.nil?
value
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ae35781c866..4f3df414d8c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -41,6 +41,11 @@ msgid_plural "%d commits behind"
msgstr[0] ""
msgstr[1] ""
+msgid "%d commit,"
+msgid_plural "%d commits,"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d commits"
msgstr ""
@@ -3529,6 +3534,9 @@ msgstr ""
msgid "Diff limits"
msgstr ""
+msgid "DiffsCompareBaseBranch|(base)"
+msgstr ""
+
msgid "Diffs|No file name available"
msgstr ""
@@ -5010,6 +5018,9 @@ msgstr ""
msgid "Help page text and support page url."
msgstr ""
+msgid "Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}"
+msgstr ""
+
msgid "Hide archived projects"
msgstr ""
@@ -5994,6 +6005,9 @@ msgstr ""
msgid "Manual job"
msgstr ""
+msgid "ManualOrdering|Couldn't save the order of the issues"
+msgstr ""
+
msgid "Map a FogBugz account ID to a GitLab user"
msgstr ""
@@ -9182,6 +9196,9 @@ msgstr ""
msgid "Show command"
msgstr ""
+msgid "Show comments"
+msgstr ""
+
msgid "Show comments only"
msgstr ""
@@ -12675,6 +12692,9 @@ msgstr ""
msgid "verify ownership"
msgstr ""
+msgid "version %{versionIndex}"
+msgstr ""
+
msgid "via %{closed_via}"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
index f6f0468e76e..796de44a012 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/49
- context 'Create', :smoke, :quarantine do
+ context 'Create', :smoke do
describe 'Snippet creation' do
it 'User creates a snippet' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -13,7 +12,7 @@ module QA
Resource::Snippet.fabricate_via_browser_ui! do |snippet|
snippet.title = 'Snippet title'
snippet.description = 'Snippet description'
- snippet.visibility = 'Public'
+ snippet.visibility = 'Private'
snippet.file_name = 'New snippet file name'
snippet.file_content = 'Snippet file text'
end
@@ -21,8 +20,7 @@ module QA
Page::Dashboard::Snippet::Show.perform do |snippet|
expect(snippet).to have_snippet_title('Snippet title')
expect(snippet).to have_snippet_description('Snippet description')
- expect(snippet).to have_embed_type('Embed')
- expect(snippet).to have_visibility_type('Public')
+ expect(snippet).to have_visibility_type('Private')
expect(snippet).to have_file_name('New snippet file name')
expect(snippet).to have_file_content('Snippet file text')
end
diff --git a/spec/controllers/concerns/continue_params_spec.rb b/spec/controllers/concerns/continue_params_spec.rb
index 5e47f5e9f28..b4b62cbe1e3 100644
--- a/spec/controllers/concerns/continue_params_spec.rb
+++ b/spec/controllers/concerns/continue_params_spec.rb
@@ -18,6 +18,14 @@ describe ContinueParams do
ActionController::Parameters.new(continue: params)
end
+ it 'returns an empty hash if params are not present' do
+ allow(controller).to receive(:params) do
+ ActionController::Parameters.new
+ end
+
+ expect(controller.continue_params).to eq({})
+ end
+
it 'cleans up any params that are not allowed' do
allow(controller).to receive(:params) do
strong_continue_params(to: '/hello',
diff --git a/spec/controllers/concerns/internal_redirect_spec.rb b/spec/controllers/concerns/internal_redirect_spec.rb
index 97119438ca1..da68c8c8697 100644
--- a/spec/controllers/concerns/internal_redirect_spec.rb
+++ b/spec/controllers/concerns/internal_redirect_spec.rb
@@ -15,44 +15,71 @@ describe InternalRedirect do
subject(:controller) { controller_class.new }
describe '#safe_redirect_path' do
- it 'is `nil` for invalid uris' do
- expect(controller.safe_redirect_path('Hello world')).to be_nil
+ where(:input) do
+ [
+ 'Hello world',
+ '//example.com/hello/world',
+ 'https://example.com/hello/world'
+ ]
end
- it 'is `nil` for paths trying to include a host' do
- expect(controller.safe_redirect_path('//example.com/hello/world')).to be_nil
+ with_them 'being invalid' do
+ it 'returns nil' do
+ expect(controller.safe_redirect_path(input)).to be_nil
+ end
end
- it 'returns the path if it is valid' do
- expect(controller.safe_redirect_path('/hello/world')).to eq('/hello/world')
+ where(:input) do
+ [
+ '/hello/world',
+ '/-/ide/project/path'
+ ]
end
- it 'returns the path with querystring if it is valid' do
- expect(controller.safe_redirect_path('/hello/world?hello=world#L123'))
- .to eq('/hello/world?hello=world#L123')
+ with_them 'being valid' do
+ it 'returns the path' do
+ expect(controller.safe_redirect_path(input)).to eq(input)
+ end
+
+ it 'returns the path with querystring and fragment' do
+ expect(controller.safe_redirect_path("#{input}?hello=world#L123"))
+ .to eq("#{input}?hello=world#L123")
+ end
end
end
describe '#safe_redirect_path_for_url' do
- it 'is `nil` for invalid urls' do
- expect(controller.safe_redirect_path_for_url('Hello world')).to be_nil
+ where(:input) do
+ [
+ 'Hello world',
+ 'http://example.com/hello/world',
+ 'http://test.host:3000/hello/world'
+ ]
end
- it 'is `nil` for urls from a with a different host' do
- expect(controller.safe_redirect_path_for_url('http://example.com/hello/world')).to be_nil
+ with_them 'being invalid' do
+ it 'returns nil' do
+ expect(controller.safe_redirect_path_for_url(input)).to be_nil
+ end
end
- it 'is `nil` for urls from a with a different port' do
- expect(controller.safe_redirect_path_for_url('http://test.host:3000/hello/world')).to be_nil
+ where(:input) do
+ [
+ 'http://test.host/hello/world'
+ ]
end
- it 'returns the path if the url is on the same host' do
- expect(controller.safe_redirect_path_for_url('http://test.host/hello/world')).to eq('/hello/world')
- end
+ with_them 'being on the same host' do
+ let(:path) { URI(input).path }
- it 'returns the path including querystring if the url is on the same host' do
- expect(controller.safe_redirect_path_for_url('http://test.host/hello/world?hello=world#L123'))
- .to eq('/hello/world?hello=world#L123')
+ it 'returns the path' do
+ expect(controller.safe_redirect_path_for_url(input)).to eq(path)
+ end
+
+ it 'returns the path with querystring and fragment' do
+ expect(controller.safe_redirect_path_for_url("#{input}?hello=world#L123"))
+ .to eq("#{path}?hello=world#L123")
+ end
end
end
@@ -82,12 +109,16 @@ describe InternalRedirect do
end
describe '#host_allowed?' do
- it 'allows uris with the same host and port' do
+ it 'allows URI with the same host and port' do
expect(controller.host_allowed?(URI('http://test.host/test'))).to be(true)
end
- it 'rejects uris with other host and port' do
+ it 'rejects URI with other host' do
expect(controller.host_allowed?(URI('http://example.com/test'))).to be(false)
end
+
+ it 'rejects URI with other port' do
+ expect(controller.host_allowed?(URI('http://test.host:3000/test'))).to be(false)
+ end
end
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 3423fdf4c41..5ac5279e997 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -115,24 +115,34 @@ describe Projects::ForksController do
end
describe 'POST create' do
- def post_create
+ def post_create(params = {})
post :create,
params: {
namespace_id: project.namespace,
project_id: project,
namespace_key: user.namespace.id
- }
+ }.merge(params)
end
context 'when user is signed in' do
- it 'responds with status 302' do
+ before do
sign_in(user)
+ end
+ it 'responds with status 302' do
post_create
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
end
+
+ it 'passes continue params to the redirect' do
+ continue_params = { to: '/-/ide/project/path', notice: 'message' }
+ post_create continue: continue_params
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params))
+ end
end
context 'when user is not signed in' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 9a598790ff2..faf3c990cb2 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -6,7 +6,8 @@ describe RegistrationsController do
include TermsHelper
describe '#create' do
- let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } }
+ let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } }
+ let(:user_params) { { user: base_user_params } }
context 'email confirmation' do
around do |example|
@@ -105,6 +106,20 @@ describe RegistrationsController do
expect(subject.current_user.terms_accepted?).to be(true)
end
end
+
+ it "logs a 'User Created' message" do
+ stub_feature_flags(registrations_recaptcha: false)
+
+ expect(Gitlab::AppLogger).to receive(:info).with(/\AUser Created: username=new_username email=new@user.com.+\z/).and_call_original
+
+ post(:create, params: user_params)
+ end
+
+ it 'handles when params are new_user' do
+ post(:create, params: { new_user: base_user_params })
+
+ expect(subject.current_user).not_to be_nil
+ end
end
describe '#destroy' do
diff --git a/spec/factories/namespace/aggregation_schedules.rb b/spec/factories/namespace/aggregation_schedules.rb
new file mode 100644
index 00000000000..c172c3360e2
--- /dev/null
+++ b/spec/factories/namespace/aggregation_schedules.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :namespace_aggregation_schedules, class: Namespace::AggregationSchedule do
+ namespace
+ end
+end
diff --git a/spec/factories/namespace/root_storage_statistics.rb b/spec/factories/namespace/root_storage_statistics.rb
new file mode 100644
index 00000000000..54c5921eb44
--- /dev/null
+++ b/spec/factories/namespace/root_storage_statistics.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :namespace_root_storage_statistics, class: Namespace::RootStorageStatistics do
+ namespace
+ end
+end
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb
index 6feafa5ece9..0cfc6e3aa46 100644
--- a/spec/factories/namespaces.rb
+++ b/spec/factories/namespaces.rb
@@ -19,5 +19,13 @@ FactoryBot.define do
owner.namespace = namespace
end
end
+
+ trait :with_aggregation_schedule do
+ association :aggregation_schedule, factory: :namespace_aggregation_schedules
+ end
+
+ trait :with_root_storage_statistics do
+ association :root_storage_statistics, factory: :namespace_root_storage_statistics
+ end
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 176f4a668ff..f390627405a 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Group issues page' do
include FilteredSearchHelpers
+ include DragTo
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group)}
@@ -99,4 +100,55 @@ describe 'Group issues page' do
end
end
end
+
+ context 'manual ordering' do
+ before do
+ stub_feature_flags(manual_sorting: true)
+ end
+
+ let!(:issue1) { create(:issue, project: project, title: 'Issue #1') }
+ let!(:issue2) { create(:issue, project: project, title: 'Issue #2') }
+ let!(:issue3) { create(:issue, project: project, title: 'Issue #3') }
+
+ it 'displays all issues' do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ page.within('.issues-list') do
+ expect(page).to have_selector('li.issue', count: 3)
+ end
+ end
+
+ it 'has manual-ordering css applied' do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ expect(page).to have_selector('.manual-ordering')
+ end
+
+ it 'each issue item has a user-can-drag css applied' do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ page.within('.manual-ordering') do
+ expect(page).to have_selector('.issue.user-can-drag', count: 3)
+ end
+ end
+
+ it 'issues should be draggable and persist order', :js do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ drag_to(selector: '.manual-ordering',
+ scrollable: '#board-app',
+ list_from_index: 0,
+ from_index: 0,
+ to_index: 2,
+ list_to_index: 0)
+
+ wait_for_requests
+
+ page.within('.manual-ordering') do
+ expect(find('.issue:nth-child(1) .title')).to have_content('Issue #2')
+ expect(find('.issue:nth-child(2) .title')).to have_content('Issue #3')
+ expect(find('.issue:nth-child(3) .title')).to have_content('Issue #1')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
index edbab14f7c1..b08ccdc2a7c 100644
--- a/spec/features/projects/environments/environment_metrics_spec.rb
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -9,11 +9,11 @@ describe 'Environment > Metrics' do
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:environment) { create(:environment, project: project) }
let(:current_time) { Time.now.utc }
+ let!(:staging) { create(:environment, name: 'staging', project: project) }
before do
project.add_developer(user)
- create(:deployment, environment: environment, deployable: build)
- stub_all_prometheus_requests(environment.slug)
+ stub_any_prometheus_request
sign_in(user)
visit_environment(environment)
@@ -23,15 +23,50 @@ describe 'Environment > Metrics' do
Timecop.freeze(current_time) { example.run }
end
+ shared_examples 'has environment selector' do
+ it 'has a working environment selector', :js do
+ click_link('See metrics')
+
+ expect(page).to have_metrics_path(environment)
+ expect(page).to have_css('div.js-environments-dropdown')
+
+ within('div.js-environments-dropdown') do
+ # Click on the dropdown
+ click_on(environment.name)
+
+ # Select the staging environment
+ click_on(staging.name)
+ end
+
+ expect(page).to have_metrics_path(staging)
+
+ wait_for_requests
+ end
+ end
+
+ context 'without deployments' do
+ it_behaves_like 'has environment selector'
+ end
+
context 'with deployments and related deployable present' do
+ before do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
it 'shows metrics' do
click_link('See metrics')
expect(page).to have_css('div#prometheus-graphs')
end
+
+ it_behaves_like 'has environment selector'
end
def visit_environment(environment)
visit project_environment_path(environment.project, environment)
end
+
+ def have_metrics_path(environment)
+ have_current_path(metrics_project_environment_path(project, id: environment.id))
+ end
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 683268d064a..e0fa9dbb5fa 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -118,19 +118,31 @@ describe 'Projects > Files > User edits files', :js do
wait_for_requests
end
- it 'inserts a content of a file in a forked project' do
- click_link('.gitignore')
- find('.js-edit-blob').click
-
+ def expect_fork_prompt
expect(page).to have_link('Fork')
expect(page).to have_button('Cancel')
+ expect(page).to have_content(
+ "You're not allowed to edit files in this project directly. "\
+ "Please fork this project, make your changes there, and submit a merge request."
+ )
+ end
- click_link('Fork')
-
+ def expect_fork_status
expect(page).to have_content(
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
)
+ end
+
+ it 'inserts a content of a file in a forked project' do
+ click_link('.gitignore')
+ click_button('Edit')
+
+ expect_fork_prompt
+
+ click_link('Fork')
+
+ expect_fork_status
find('.file-editor', match: :first)
@@ -140,12 +152,24 @@ describe 'Projects > Files > User edits files', :js do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
end
+ it 'opens the Web IDE in a forked project' do
+ click_link('.gitignore')
+ click_button('Web IDE')
+
+ expect_fork_prompt
+
+ click_link('Fork')
+
+ expect_fork_status
+
+ expect(page).to have_css('.ide .multi-file-tab', text: '.gitignore')
+ end
+
it 'commits an edited file in a forked project' do
click_link('.gitignore')
find('.js-edit-blob').click
- expect(page).to have_link('Fork')
- expect(page).to have_button('Cancel')
+ expect_fork_prompt
click_link('Fork')
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 8a6901ea4e9..50befa7028d 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -90,7 +90,7 @@ describe 'Signup' do
expect(page).to have_content("Invalid input, please avoid emojis")
end
- it 'shows a pending message if the username availability is being fetched' do
+ it 'shows a pending message if the username availability is being fetched', :quarantine do
fill_in 'new_user_username', with: 'new-user'
expect(find('.username > .validation-pending')).not_to have_css '.hide'
diff --git a/spec/frontend/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js
index 4dd27e94d97..5b5ae4b6556 100644
--- a/spec/frontend/boards/modal_store_spec.js
+++ b/spec/frontend/boards/modal_store_spec.js
@@ -25,7 +25,7 @@ describe('Modal store', () => {
});
issue2 = new ListIssue({
title: 'Testing',
- id: 1,
+ id: 2,
iid: 2,
confidential: false,
labels: [],
diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/services/board_service_spec.js
new file mode 100644
index 00000000000..de9fc998360
--- /dev/null
+++ b/spec/frontend/boards/services/board_service_spec.js
@@ -0,0 +1,390 @@
+import BoardService from '~/boards/services/board_service';
+import { TEST_HOST } from 'helpers/test_constants';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+
+describe('BoardService', () => {
+ const dummyResponse = "without type checking this doesn't matter";
+ const boardId = 'dummy-board-id';
+ const endpoints = {
+ boardsEndpoint: `${TEST_HOST}/boards`,
+ listsEndpoint: `${TEST_HOST}/lists`,
+ bulkUpdatePath: `${TEST_HOST}/bulk/update`,
+ recentBoardsEndpoint: `${TEST_HOST}/recent/boards`,
+ };
+
+ let service;
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ service = new BoardService({
+ ...endpoints,
+ boardId,
+ });
+ });
+
+ describe('all', () => {
+ it('makes a request to fetch lists', () => {
+ axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.all()).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500);
+
+ return expect(service.all()).rejects.toThrow();
+ });
+ });
+
+ describe('generateDefaultLists', () => {
+ const listsEndpointGenerate = `${endpoints.listsEndpoint}/generate.json`;
+
+ it('makes a request to generate default lists', () => {
+ axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.generateDefaultLists()).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onPost(listsEndpointGenerate).replyOnce(500);
+
+ return expect(service.generateDefaultLists()).rejects.toThrow();
+ });
+ });
+
+ describe('createList', () => {
+ const entityType = 'moorhen';
+ const entityId = 'quack';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({ list: { [entityType]: entityId } }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(endpoints.listsEndpoint).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.createList(entityId, entityType))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.createList(entityId, entityType))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('updateList', () => {
+ const id = 'David Webb';
+ const position = 'unknown';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({ list: { position } }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to update a list position', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.updateList(id, position))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.updateList(id, position))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('destroyList', () => {
+ const id = '-42';
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock
+ .onDelete(`${endpoints.listsEndpoint}/${id}`)
+ .replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to delete a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.destroyList(id))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.destroyList(id))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('getIssuesForList', () => {
+ const id = 'TOO-MUCH';
+ const url = `${endpoints.listsEndpoint}/${id}/issues?id=${id}`;
+
+ it('makes a request to fetch list issues', () => {
+ axiosMock.onGet(url).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getIssuesForList(id)).resolves.toEqual(expectedResponse);
+ });
+
+ it('makes a request to fetch list issues with filter', () => {
+ const filter = { algal: 'scrubber' };
+ axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(url).replyOnce(500);
+
+ return expect(service.getIssuesForList(id)).rejects.toThrow();
+ });
+ });
+
+ describe('moveIssue', () => {
+ const urlRoot = 'potato';
+ const id = 'over 9000';
+ const fromListId = 'left';
+ const toListId = 'right';
+ const moveBeforeId = 'up';
+ const moveAfterId = 'down';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ from_list_id: fromListId,
+ to_list_id: toListId,
+ move_before_id: moveBeforeId,
+ move_after_id: moveAfterId,
+ }),
+ });
+
+ let requestSpy;
+
+ beforeAll(() => {
+ global.gon.relative_url_root = urlRoot;
+ });
+
+ afterAll(() => {
+ delete global.gon.relative_url_root;
+ });
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock
+ .onPut(`${urlRoot}/-/boards/${boardId}/issues/${id}`)
+ .replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to move an issue between lists', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('newIssue', () => {
+ const id = 'not-creative';
+ const issue = { some: 'issue data' };
+ const url = `${endpoints.listsEndpoint}/${id}/issues`;
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ issue,
+ }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(url).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a new issue', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.newIssue(id, issue))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.newIssue(id, issue))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('getBacklog', () => {
+ const urlRoot = 'deep';
+ const url = `${urlRoot}/-/boards/${boardId}/issues.json?not=relevant`;
+ const requestParams = {
+ not: 'relevant',
+ };
+
+ beforeAll(() => {
+ global.gon.relative_url_root = urlRoot;
+ });
+
+ afterAll(() => {
+ delete global.gon.relative_url_root;
+ });
+
+ it('makes a request to fetch backlog', () => {
+ axiosMock.onGet(url).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(url).replyOnce(500);
+
+ return expect(service.getBacklog(requestParams)).rejects.toThrow();
+ });
+ });
+
+ describe('bulkUpdate', () => {
+ const issueIds = [1, 2, 3];
+ const extraData = { moar: 'data' };
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ update: {
+ ...extraData,
+ issuable_ids: '1,2,3',
+ },
+ }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.bulkUpdate(issueIds, extraData))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.bulkUpdate(issueIds, extraData))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('getIssueInfo', () => {
+ const dummyEndpoint = `${TEST_HOST}/some/where`;
+
+ it('makes a request to the given endpoint', () => {
+ axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(BoardService.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(dummyEndpoint).replyOnce(500);
+
+ return expect(BoardService.getIssueInfo(dummyEndpoint)).rejects.toThrow();
+ });
+ });
+
+ describe('toggleIssueSubscription', () => {
+ const dummyEndpoint = `${TEST_HOST}/some/where`;
+
+ it('makes a request to the given endpoint', () => {
+ axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
+ expectedResponse,
+ );
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onPost(dummyEndpoint).replyOnce(500);
+
+ return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
index c146ef79be7..8632c5c4e26 100644
--- a/spec/frontend/clusters/services/application_state_machine_spec.js
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -72,9 +72,10 @@ describe('applicationStateMachine', () => {
describe(`current state is ${INSTALLABLE}`, () => {
it.each`
- expectedState | event | effects
- ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
- ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -108,9 +109,10 @@ describe('applicationStateMachine', () => {
describe(`current state is ${INSTALLED}`, () => {
it.each`
- expectedState | event | effects
- ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
- ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -119,7 +121,7 @@ describe('applicationStateMachine', () => {
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
- ...effects,
+ ...noEffectsToEmptyObject(effects),
});
});
});
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
new file mode 100644
index 00000000000..2b7dffdcd88
--- /dev/null
+++ b/spec/frontend/ide/utils_spec.js
@@ -0,0 +1,44 @@
+import { commitItemIconMap } from '~/ide/constants';
+import { getCommitIconMap } from '~/ide/utils';
+import { decorateData } from '~/ide/stores/utils';
+
+describe('WebIDE utils', () => {
+ const createFile = (name = 'name', id = name, type = '', parent = null) =>
+ decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: parent ? `${parent.path}/${name}` : name,
+ parentPath: parent ? parent.path : '',
+ lastCommit: {},
+ });
+
+ describe('getCommitIconMap', () => {
+ let entry;
+
+ beforeEach(() => {
+ entry = createFile('Entry item');
+ });
+
+ it('renders "deleted" icon for deleted entries', () => {
+ entry.deleted = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.deleted);
+ });
+ it('renders "addition" icon for temp entries', () => {
+ entry.tempFile = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.addition);
+ });
+ it('renders "modified" icon for newly-renamed entries', () => {
+ entry.prevPath = 'foo/bar';
+ entry.tempFile = false;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified);
+ });
+ it('renders "modified" icon even for temp entries if they are newly-renamed', () => {
+ entry.prevPath = 'foo/bar';
+ entry.tempFile = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified);
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 1f06d693411..d55dc553031 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -29,10 +29,20 @@ exports[`Repository table row component renders table row 1`] = `
<td
class="d-none d-sm-table-cell tree-commit"
- />
+ >
+ <glskeletonloading-stub
+ class="h-auto"
+ lines="1"
+ />
+ </td>
<td
class="tree-time-ago text-right"
- />
+ >
+ <glskeletonloading-stub
+ class="ml-auto h-auto w-50"
+ lines="1"
+ />
+ </td>
</tr>
`;
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 5a345ddeacd..c566057ad3f 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -16,6 +16,8 @@ function factory(propsData = {}) {
vm = shallowMount(TableRow, {
propsData: {
...propsData,
+ name: propsData.path,
+ projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
},
mocks: {
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
new file mode 100644
index 00000000000..a9499f7c61b
--- /dev/null
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -0,0 +1,129 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { normalizeData, resolveCommit, fetchLogsTree } from '~/repository/log_tree';
+
+const mockData = [
+ {
+ commit: {
+ id: '123',
+ message: 'testing message',
+ committed_date: '2019-01-01',
+ },
+ commit_path: `https://test.com`,
+ file_name: 'index.js',
+ type: 'blob',
+ },
+];
+
+describe('normalizeData', () => {
+ it('normalizes data into LogTreeCommit object', () => {
+ expect(normalizeData(mockData)).toEqual([
+ {
+ sha: '123',
+ message: 'testing message',
+ committedDate: '2019-01-01',
+ commitPath: 'https://test.com',
+ fileName: 'index.js',
+ type: 'blob',
+ __typename: 'LogTreeCommit',
+ },
+ ]);
+ });
+});
+
+describe('resolveCommit', () => {
+ it('calls resolve when commit found', () => {
+ const resolver = {
+ entry: { name: 'index.js', type: 'blob' },
+ resolve: jest.fn(),
+ };
+ const commits = [{ fileName: 'index.js', type: 'blob' }];
+
+ resolveCommit(commits, resolver);
+
+ expect(resolver.resolve).toHaveBeenCalledWith({ fileName: 'index.js', type: 'blob' });
+ });
+});
+
+describe('fetchLogsTree', () => {
+ let mock;
+ let client;
+ let resolver;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(/(.*)/).reply(200, mockData, {});
+
+ jest.spyOn(axios, 'get');
+
+ global.gon = { gitlab_url: 'https://test.com' };
+
+ client = {
+ readQuery: () => ({
+ projectPath: 'gitlab-org/gitlab-ce',
+ ref: 'master',
+ commits: [],
+ }),
+ writeQuery: jest.fn(),
+ };
+
+ resolver = {
+ entry: { name: 'index.js', type: 'blob' },
+ resolve: jest.fn(),
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('calls axios get', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(axios.get).toHaveBeenCalledWith(
+ 'https://test.com/gitlab-org/gitlab-ce/refs/master/logs_tree',
+ { params: { format: 'json', offset: '0' } },
+ );
+ }));
+
+ it('calls axios get once', () =>
+ Promise.all([
+ fetchLogsTree(client, '', '0', resolver),
+ fetchLogsTree(client, '', '0', resolver),
+ ]).then(() => {
+ expect(axios.get.mock.calls.length).toEqual(1);
+ }));
+
+ it('calls entry resolver', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(resolver.resolve).toHaveBeenCalledWith({
+ __typename: 'LogTreeCommit',
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ message: 'testing message',
+ sha: '123',
+ type: 'blob',
+ });
+ }));
+
+ it('writes query to client', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: expect.anything(),
+ data: {
+ commits: [
+ {
+ __typename: 'LogTreeCommit',
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ message: 'testing message',
+ sha: '123',
+ type: 'blob',
+ },
+ ],
+ },
+ });
+ }));
+});
diff --git a/spec/helpers/recaptcha_experiment_helper_spec.rb b/spec/helpers/recaptcha_experiment_helper_spec.rb
new file mode 100644
index 00000000000..775c2caa082
--- /dev/null
+++ b/spec/helpers/recaptcha_experiment_helper_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RecaptchaExperimentHelper, type: :helper do
+ describe '.show_recaptcha_sign_up?' do
+ context 'when reCAPTCHA is disabled' do
+ it 'returns false' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect(helper.show_recaptcha_sign_up?).to be(false)
+ end
+ end
+
+ context 'when reCAPTCHA is enabled' do
+ it 'returns true' do
+ stub_application_setting(recaptcha_enabled: true)
+
+ expect(helper.show_recaptcha_sign_up?).to be(true)
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
index f63007c7dd2..554bd1ae3b5 100644
--- a/spec/javascripts/ide/components/ide_tree_list_spec.js
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -58,6 +58,20 @@ describe('IDE tree list', () => {
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
+
+ it('does not render moved entries', done => {
+ const tree = [file('moved entry'), file('normal entry')];
+ tree[0].moved = true;
+ store.state.trees['abcproject/master'].tree = tree;
+ const container = vm.$el.querySelector('.ide-tree-body');
+
+ vm.$nextTick(() => {
+ expect(container.children.length).toBe(1);
+ expect(vm.$el.textContent).not.toContain('moved entry');
+ expect(vm.$el.textContent).toContain('normal entry');
+ done();
+ });
+ });
});
describe('empty-branch state', () => {
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index dd2313dc800..021c3076094 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -275,6 +275,43 @@ describe('IDE store file actions', () => {
});
});
+ describe('Re-named success', () => {
+ beforeEach(() => {
+ localFile = file(`newCreate-${Math.random()}`);
+ localFile.url = `project/getFileDataURL`;
+ localFile.prevPath = 'old-dull-file';
+ localFile.path = 'new-shiny-file';
+ store.state.entries[localFile.path] = localFile;
+
+ mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce(
+ 200,
+ {
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ },
+ {
+ 'page-title': 'testing old-dull-file',
+ },
+ );
+ });
+
+ it('sets document title considering `prevPath` on a file', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(document.title).toBe('testing new-shiny-file');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
describe('error', () => {
beforeEach(() => {
mock.onGet(`project/getFileDataURL`).networkError();
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 537152f5eed..2d105103c1c 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -536,8 +536,15 @@ describe('Multi-file store actions', () => {
type: types.RENAME_ENTRY,
payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
},
+ {
+ type: types.TOGGLE_FILE_CHANGED,
+ payload: {
+ file: store.state.entries['parent-path/new-name'],
+ changed: true,
+ },
+ },
],
- [{ type: 'deleteEntry', payload: 'test' }, { type: 'triggerFilesChange' }],
+ [{ type: 'triggerFilesChange' }],
done,
);
});
@@ -584,7 +591,6 @@ describe('Multi-file store actions', () => {
parentPath: 'parent-path/new-name',
},
},
- { type: 'deleteEntry', payload: 'test' },
{ type: 'triggerFilesChange' },
],
done,
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 5ee098bf17f..460c5b01081 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -309,7 +309,7 @@ describe('Multi-file store mutations', () => {
...localState.entries.oldPath,
id: 'newPath',
name: 'newPath',
- key: 'newPath-blob-name',
+ key: 'newPath-blob-oldPath',
path: 'newPath',
tempFile: true,
prevPath: 'oldPath',
@@ -318,6 +318,7 @@ describe('Multi-file store mutations', () => {
url: `${gl.TEST_HOST}/newPath`,
moved: jasmine.anything(),
movedPath: jasmine.anything(),
+ opened: false,
});
});
@@ -349,13 +350,5 @@ describe('Multi-file store mutations', () => {
expect(localState.entries.parentPath.tree.length).toBe(1);
});
-
- it('adds to openFiles if previously opened', () => {
- localState.entries.oldPath.opened = true;
-
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
-
- expect(localState.openFiles).toEqual([localState.entries.newPath]);
- });
});
});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index f4166987aed..cf72cce1781 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -62,16 +62,34 @@ describe('Dashboard', () => {
});
describe('no metrics are available yet', () => {
- it('shows a getting started empty state when no metrics are present', () => {
+ beforeEach(() => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData },
store,
});
+ });
+ it('shows a getting started empty state when no metrics are present', () => {
expect(component.$el.querySelector('.prometheus-graphs')).toBe(null);
expect(component.emptyState).toEqual('gettingStarted');
});
+
+ it('shows the environment selector', () => {
+ expect(component.$el.querySelector('.js-environments-dropdown')).toBeTruthy();
+ });
+ });
+
+ describe('no data found', () => {
+ it('shows the environment selector dropdown', () => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, showEmptyState: true },
+ store,
+ });
+
+ expect(component.$el.querySelector('.js-environments-dropdown')).toBeTruthy();
+ });
});
describe('requests information to the server', () => {
@@ -150,14 +168,24 @@ describe('Dashboard', () => {
singleGroupResponse,
);
- setTimeout(() => {
- const dropdownMenuEnvironments = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item',
- );
+ Vue.nextTick()
+ .then(() => {
+ const dropdownMenuEnvironments = component.$el.querySelectorAll(
+ '.js-environments-dropdown .dropdown-item',
+ );
- expect(dropdownMenuEnvironments.length).toEqual(component.environments.length);
- done();
- });
+ expect(component.environments.length).toEqual(environmentData.length);
+ expect(dropdownMenuEnvironments.length).toEqual(component.environments.length);
+
+ Array.from(dropdownMenuEnvironments).forEach((value, index) => {
+ if (environmentData[index].metrics_path) {
+ expect(value).toHaveAttr('href', environmentData[index].metrics_path);
+ }
+ });
+
+ done();
+ })
+ .catch(done.fail);
});
it('hides the environments dropdown list when there is no environments', done => {
@@ -212,7 +240,7 @@ describe('Dashboard', () => {
Vue.nextTick()
.then(() => {
const dropdownItems = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item[active="true"]',
+ '.js-environments-dropdown .dropdown-item.is-active',
);
expect(dropdownItems.length).toEqual(1);
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 6d6107ca3e7..ba6abba4e61 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -223,6 +223,19 @@ describe Gitlab::GitalyClient::CommitService do
end
context 'when caching of the ref name is enabled' do
+ it 'caches negative entries' do
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: nil))
+
+ commit = nil
+ 2.times do
+ ::Gitlab::GitalyClient.allow_ref_name_caching do
+ commit = described_class.new(repository).find_commit('master')
+ end
+ end
+
+ expect(commit).to eq(nil)
+ end
+
it 'returns a cached commit' do
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: commit_dbl))
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
index 13cf52fd795..20842f55014 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
@@ -34,12 +34,6 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
end
end
- describe '#authorized_find' do
- it 'returns the object' do
- expect(loading_resource.authorized_find).to eq(project)
- end
- end
-
describe '#authorized_find!' do
it 'returns the object' do
expect(loading_resource.authorized_find!).to eq(project)
@@ -66,12 +60,6 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
end
end
- describe '#authorized_find' do
- it 'returns `nil`' do
- expect(loading_resource.authorized_find).to be_nil
- end
- end
-
describe '#authorized_find!' do
it 'raises an error' do
expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
@@ -101,6 +89,45 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
end
end
+ context 'when the class does not define authorize' do
+ let(:fake_class) do
+ Class.new do
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ attr_reader :user, :found_object
+
+ def initialize(user, found_object)
+ @user, @found_object = user, found_object
+ end
+
+ def find_object(*_args)
+ found_object
+ end
+
+ def current_user
+ user
+ end
+
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+ let(:error) { /#{fake_class.name} has no authorizations/ }
+
+ describe '#authorized_find!' do
+ it 'raises a comprehensive error message' do
+ expect { loading_resource.authorized_find! }.to raise_error(error)
+ end
+ end
+
+ describe '#authorized?' do
+ it 'raises a comprehensive error message' do
+ expect { loading_resource.authorized?(project) }.to raise_error(error)
+ end
+ end
+ end
+
describe '#authorize' do
it 'adds permissions from subclasses to those of superclasses when used on classes' do
base_class = Class.new do
diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb
index 59160741c45..39cdd42088e 100644
--- a/spec/lib/gitlab/json_cache_spec.rb
+++ b/spec/lib/gitlab/json_cache_spec.rb
@@ -129,19 +129,52 @@ describe Gitlab::JsonCache do
.with(expanded_key)
.and_return(nil)
+ expect(ActiveSupport::JSON).not_to receive(:decode)
expect(cache.read(key)).to be_nil
end
- context 'when the cached value is a boolean' do
+ context 'when the cached value is true' do
it 'parses the cached value' do
allow(backend).to receive(:read)
.with(expanded_key)
.and_return(true)
+ expect(ActiveSupport::JSON).to receive(:decode).with("true").and_call_original
expect(cache.read(key, BroadcastMessage)).to eq(true)
end
end
+ context 'when the cached value is false' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(false)
+
+ expect(ActiveSupport::JSON).to receive(:decode).with("false").and_call_original
+ expect(cache.read(key, BroadcastMessage)).to eq(false)
+ end
+ end
+
+ context 'when the cached value is a JSON true value' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return("true")
+
+ expect(cache.read(key, BroadcastMessage)).to eq(true)
+ end
+ end
+
+ context 'when the cached value is a JSON false value' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return("false")
+
+ expect(cache.read(key, BroadcastMessage)).to eq(false)
+ end
+ end
+
context 'when the cached value is a hash' do
it 'parses the cached value' do
allow(backend).to receive(:read)
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 4d53e4aad8a..020ada3c47a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -48,14 +48,14 @@ describe BroadcastMessage do
expect(described_class.current).to be_empty
end
- it 'caches the output of the query' do
+ it 'caches the output of the query for two weeks' do
create(:broadcast_message)
- expect(described_class).to receive(:current_and_future_messages).and_call_original.once
+ expect(described_class).to receive(:current_and_future_messages).and_call_original.twice
described_class.current
- Timecop.travel(1.year) do
+ Timecop.travel(3.weeks) do
described_class.current
end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 806b4f61bd8..28630f7d3fe 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -158,7 +158,7 @@ describe InternalId do
before do
described_class.reset_column_information
# Project factory will also call the current_version
- expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
end
it 'does not reset any of the iids' do
diff --git a/spec/models/namespace/aggregation_schedule_spec.rb b/spec/models/namespace/aggregation_schedule_spec.rb
new file mode 100644
index 00000000000..5ba7547ff4d
--- /dev/null
+++ b/spec/models/namespace/aggregation_schedule_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespace::AggregationSchedule, type: :model do
+ it { is_expected.to belong_to :namespace }
+end
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
new file mode 100644
index 00000000000..f6fb5af5aae
--- /dev/null
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespace::RootStorageStatistics, type: :model do
+ it { is_expected.to belong_to :namespace }
+ it { is_expected.to have_one(:route).through(:namespace) }
+
+ it { is_expected.to delegate_method(:all_projects).to(:namespace) }
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index d80183af33e..30e49cf204f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -15,6 +15,8 @@ describe Namespace do
it { is_expected.to have_many :project_statistics }
it { is_expected.to belong_to :parent }
it { is_expected.to have_many :children }
+ it { is_expected.to have_one :root_storage_statistics }
+ it { is_expected.to have_one :aggregation_schedule }
end
describe 'validations' do
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index d5f77f3354b..8d43ce4f662 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -34,8 +34,12 @@ describe PagesDomains::ObtainLetsEncryptCertificateService do
end
context 'when there is no acme order' do
- it 'creates acme order' do
+ it 'creates acme order and schedules next step' do
expect_to_create_acme_challenge
+ expect(PagesDomainSslRenewalWorker).to(
+ receive(:perform_in).with(described_class::CHALLENGE_PROCESSING_DELAY, pages_domain.id)
+ .and_return(nil).once
+ )
service.execute
end
@@ -82,8 +86,12 @@ describe PagesDomains::ObtainLetsEncryptCertificateService do
stub_lets_encrypt_order(existing_order.url, 'ready')
end
- it 'request certificate' do
+ it 'request certificate and schedules next step' do
expect(api_order).to receive(:request_certificate).and_call_original
+ expect(PagesDomainSslRenewalWorker).to(
+ receive(:perform_in).with(described_class::CERTIFICATE_PROCESSING_DELAY, pages_domain.id)
+ .and_return(nil).once
+ )
service.execute
end
diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb
index f93e5aae82a..2c3effec617 100644
--- a/spec/services/projects/propagate_service_template_spec.rb
+++ b/spec/services/projects/propagate_service_template_spec.rb
@@ -72,7 +72,7 @@ describe Projects::PropagateServiceTemplate do
expect(project.pushover_service.properties).to eq(service_template.properties)
end
- describe 'bulk update' do
+ describe 'bulk update', :use_sql_query_cache do
let(:project_total) { 5 }
before do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 390a869d93f..3bd2408dc72 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -218,6 +218,12 @@ RSpec.configure do |config|
ActionController::Base.cache_store = caching_store
end
+ config.around(:each, :use_sql_query_cache) do |example|
+ ActiveRecord::Base.cache do
+ example.run
+ end
+ end
+
# The :each scope runs "inside" the example, so this hook ensures the DB is in the
# correct state before any examples' before hooks are called. This prevents a
# problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index 87f825152cf..db662836013 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -70,6 +70,10 @@ module PrometheusHelpers
WebMock.stub_request(:get, url).to_raise(exception_type)
end
+ def stub_any_prometheus_request
+ WebMock.stub_request(:any, /prometheus.example.com/)
+ end
+
def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
stub_prometheus_request(
prometheus_query_with_time_url(prometheus_memory_query(environment_slug), Time.now.utc),
diff --git a/spec/support/inspect_squelch.rb b/spec/support/inspect_squelch.rb
new file mode 100644
index 00000000000..8ee6732370b
--- /dev/null
+++ b/spec/support/inspect_squelch.rb
@@ -0,0 +1,7 @@
+# This class can generate a lot of output if it fails,
+# so squelch the instance variable output.
+class ActiveSupport::Cache::NullStore
+ def inspect
+ "<#{self.class}>"
+ end
+end
diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb
index 6c4e11910d3..d1a765f27b9 100644
--- a/spec/support/sidekiq.rb
+++ b/spec/support/sidekiq.rb
@@ -30,6 +30,8 @@ RSpec.configure do |config|
end
config.after(:each, :sidekiq, :redis) do
- Sidekiq.redis { |redis| redis.flushdb }
+ Sidekiq.redis do |connection|
+ connection.redis.flushdb
+ end
end
end
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
index 0fbf36b39cc..2aadd3dbe1e 100644
--- a/vendor/jupyter/values.yaml
+++ b/vendor/jupyter/values.yaml
@@ -46,7 +46,7 @@ singleuser:
- "-c"
- >
git clone https://gitlab.com/gitlab-org/nurtch-demo.git DevOps-Runbook-Demo || true;
- echo "https://${GITLAB_USER_LOGIN}:${GITLAB_ACCESS_TOKEN}@${GITLAB_HOST}" > ~/.git-credentials;
+ echo "https://oauth2:${GITLAB_ACCESS_TOKEN}@${GITLAB_HOST}" > ~/.git-credentials;
git config --global credential.helper store;
git config --global user.email "${GITLAB_USER_EMAIL}";
git config --global user.name "${GITLAB_USER_NAME}";