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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-15 00:07:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-15 00:07:45 +0300
commit0b12a5312c9701fbfed25fbb334d47900ced736b (patch)
treea29a27e297134f573fd8e5c298d241f3156c207a
parent92f95ccac81911d1fcc32e999a7f1ce04624a56c (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue6
-rw-r--r--app/assets/javascripts/diffs/components/app.vue8
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue6
-rw-r--r--app/assets/javascripts/diffs/store/actions.js8
-rw-r--r--app/assets/javascripts/diffs/store/getters.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js11
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue25
-rw-r--r--app/assets/javascripts/ide/stores/actions.js101
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js5
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js4
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue160
-rw-r--r--app/assets/javascripts/self_monitor/index.js23
-rw-r--r--app/assets/javascripts/self_monitor/store/actions.js126
-rw-r--r--app/assets/javascripts/self_monitor/store/index.js21
-rw-r--r--app/assets/javascripts/self_monitor/store/mutation_types.js6
-rw-r--r--app/assets/javascripts/self_monitor/store/mutations.js22
-rw-r--r--app/assets/javascripts/self_monitor/store/state.js15
-rw-r--r--app/helpers/markup_helper.rb2
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml3
-rw-r--r--changelogs/unreleased/195776-fix-discard-rename-web-ide.yml5
-rw-r--r--changelogs/unreleased/34522-webide-empty-repos.yml5
-rw-r--r--changelogs/unreleased/55347-mr-diffs-file-count-increments-while-batch-loading.yml5
-rw-r--r--changelogs/unreleased/7225-no-audit-event-for-adding-a-user-to-a-group-or-project-through-api.yml5
-rw-r--r--changelogs/unreleased/ab-projects-api-more-indexes.yml5
-rw-r--r--changelogs/unreleased/add-geo-node-api.yml5
-rw-r--r--changelogs/unreleased/dblessing_update_net_ldap_gem.yml5
-rw-r--r--changelogs/unreleased/split-up-relativelinkfilter.yml5
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/migrate/20200110144316_add_indexes_for_projects_api.rb31
-rw-r--r--db/schema.rb14
-rw-r--r--doc/administration/geo/replication/troubleshooting.md2
-rw-r--r--doc/api/geo_nodes.md48
-rw-r--r--doc/development/packages.md1
-rw-r--r--doc/user/application_security/sast/index.md51
-rw-r--r--doc/user/packages/npm_registry/index.md40
-rw-r--r--lib/api/helpers/members_helpers.rb4
-rw-r--r--lib/api/members.rb4
-rw-r--r--lib/banzai/filter/base_relative_link_filter.rb45
-rw-r--r--lib/banzai/filter/repository_link_filter.rb (renamed from lib/banzai/filter/relative_link_filter.rb)83
-rw-r--r--lib/banzai/filter/upload_link_filter.rb61
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb5
-rw-r--r--lib/banzai/pipeline/relative_link_pipeline.rb13
-rw-r--r--lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb16
-rw-r--r--locale/gitlab.pot38
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb16
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb2
-rw-r--r--qa/qa/resource/deploy_key.rb4
-rw-r--r--qa/qa/resource/ssh_key.rb2
-rw-r--r--qa/qa/runtime/key/base.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb4
-rw-r--r--spec/features/markdown/markdown_spec.rb18
-rw-r--r--spec/fixtures/markdown.md.erb8
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js1
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap72
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_spec.js83
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js255
-rw-r--r--spec/frontend/self_monitor/store/mutations_spec.js64
-rw-r--r--spec/javascripts/diffs/components/app_spec.js8
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js7
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js8
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js43
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js37
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js245
-rw-r--r--spec/lib/banzai/filter/repository_link_filter_spec.rb (renamed from spec/lib/banzai/filter/relative_link_filter_spec.rb)174
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb221
-rw-r--r--spec/lib/banzai/pipeline/post_process_pipeline_spec.rb26
-rw-r--r--spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb19
-rw-r--r--spec/support/matchers/markdown_matchers.rb17
70 files changed, 1885 insertions, 511 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index f55fb554c4a..2a94b907417 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -629,7 +629,7 @@ GEM
nakayoshi_fork (0.0.4)
nap (1.1.0)
nenv (0.3.0)
- net-ldap (0.16.0)
+ net-ldap (0.16.2)
net-ntp (2.1.3)
net-ssh (5.2.0)
netrc (0.11.0)
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 74f1373f144..c856e380c41 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -115,12 +115,10 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Deploy key') }}</div>
<div class="table-mobile-content qa-key">
<strong class="title qa-key-title"> {{ deployKey.title }} </strong>
- <div class="fingerprint qa-key-fingerprint">
+ <div class="fingerprint" data-qa-selector="key_md5_fingerprint">
{{ __('MD5') }}:{{ deployKey.fingerprint }}
</div>
- <div class="fingerprint qa-key-fingerprint">
- {{ __('SHA256') }}:{{ deployKey.fingerprint_sha256 }}
- </div>
+ <div class="fingerprint">{{ __('SHA256') }}:{{ deployKey.fingerprint_sha256 }}</div>
</div>
</div>
<div class="table-section section-30 section-wrap">
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index c6d32ffef34..23b8458aa6b 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -95,6 +95,7 @@ export default {
return {
treeWidth,
+ diffFilesLength: 0,
};
},
computed: {
@@ -241,7 +242,8 @@ export default {
fetchData(toggleTree = true) {
if (this.glFeatures.diffsBatchLoad) {
this.fetchDiffFilesMeta()
- .then(() => {
+ .then(({ real_size }) => {
+ this.diffFilesLength = parseInt(real_size, 10);
if (toggleTree) this.hideTreeListIfJustOneFile();
this.startDiffRendering();
@@ -264,7 +266,8 @@ export default {
});
} else {
this.fetchDiffFiles()
- .then(() => {
+ .then(({ real_size }) => {
+ this.diffFilesLength = parseInt(real_size, 10);
if (toggleTree) {
this.hideTreeListIfJustOneFile();
}
@@ -351,6 +354,7 @@ export default {
:merge-request-diff="mergeRequestDiff"
:target-branch="targetBranch"
:is-limited-container="isLimitedContainer"
+ :diff-files-length="diffFilesLength"
/>
<hidden-files-warning
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 2e57a47f2f7..24542126b07 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -42,9 +42,13 @@ export default {
required: false,
default: false,
},
+ diffFilesLength: {
+ type: Number,
+ required: true,
+ },
},
computed: {
- ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']),
+ ...mapGetters('diffs', ['hasCollapsedFile']),
...mapState('diffs', [
'commit',
'showTreeList',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 6714f4e62b8..b920e041135 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -64,6 +64,7 @@ export const fetchDiffFiles = ({ state, commit }) => {
const urlParams = {
w: state.showWhitespace ? '0' : '1',
};
+ let returnData;
if (state.useSingleDiffStyle) {
urlParams.view = state.diffViewType;
@@ -87,9 +88,13 @@ export const fetchDiffFiles = ({ state, commit }) => {
worker.postMessage(state.diffFiles);
+ returnData = res.data;
return Vue.nextTick();
})
- .then(handleLocationHash)
+ .then(() => {
+ handleLocationHash();
+ return returnData;
+ })
.catch(() => worker.terminate());
};
@@ -147,6 +152,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => {
prepareDiffData(data);
worker.postMessage(data.diff_files);
+ return data;
})
.catch(() => worker.terminate());
};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index bc27e263bff..c4737090a70 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -95,8 +95,6 @@ export const allBlobs = (state, getters) =>
return acc;
}, []);
-export const diffFilesLength = state => state.diffFiles.length;
-
export const getCommentFormForDiffFile = state => fileHash =>
state.commentForms.find(form => form.fileHash === fileHash);
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 8cfdded1f9b..1505be1a0b2 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -179,16 +179,19 @@ export default {
const mapDiscussions = (line, extraCheck = () => true) => ({
...line,
discussions: extraCheck()
- ? line.discussions
+ ? line.discussions &&
+ line.discussions
.filter(() => !line.discussions.some(({ id }) => discussion.id === id))
.concat(lineCheck(line) ? discussion : line.discussions)
: [],
});
const setDiscussionsExpanded = line => {
- const isLineNoteTargeted = line.discussions.some(
- disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`),
- );
+ const isLineNoteTargeted =
+ line.discussions &&
+ line.discussions.some(
+ disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`),
+ );
return {
...line,
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index ecafb4e81c4..bf3d736ddf3 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -67,8 +67,8 @@ export default {
if (this.entryModal.type === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
flash(
- sprintf(s__('The name %{entryName} is already taken in this directory.'), {
- entryName: this.entryName,
+ sprintf(s__('The name "%{name}" is already taken in this directory.'), {
+ name: this.entryName,
}),
'alert',
document,
@@ -81,22 +81,11 @@ export default {
const entryName = parentPath.pop();
parentPath = parentPath.join('/');
- const createPromise =
- parentPath && !this.entries[parentPath]
- ? this.createTempEntry({ name: parentPath, type: 'tree' })
- : Promise.resolve();
-
- createPromise
- .then(() =>
- this.renameEntry({
- path: this.entryModal.entry.path,
- name: entryName,
- parentPath,
- }),
- )
- .catch(() =>
- flash(__('Error creating a new path'), 'alert', document, null, false, true),
- );
+ this.renameEntry({
+ path: this.entryModal.entry.path,
+ name: entryName,
+ parentPath,
+ });
}
} else {
this.createTempEntry({
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7ffb430296b..3445ef7a75f 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -53,60 +53,55 @@ export const setResizingStatus = ({ commit }, resizing) => {
export const createTempEntry = (
{ state, commit, dispatch },
{ name, type, content = '', base64 = false, binary = false, rawPath = '' },
-) =>
- new Promise(resolve => {
- const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
-
- if (state.entries[name] && !state.entries[name].deleted) {
- flash(
- `The name "${name.split('/').pop()}" is already taken in this directory.`,
- 'alert',
- document,
- null,
- false,
- true,
- );
-
- resolve();
-
- return null;
- }
-
- const data = decorateFiles({
- data: [fullName],
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- type,
- tempFile: true,
- content,
- base64,
- binary,
- rawPath,
- });
- const { file, parentPath } = data;
+) => {
+ const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
+
+ if (state.entries[name] && !state.entries[name].deleted) {
+ flash(
+ sprintf(__('The name "%{name}" is already taken in this directory.'), {
+ name: name.split('/').pop(),
+ }),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
- commit(types.CREATE_TMP_ENTRY, {
- data,
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- });
+ return;
+ }
- if (type === 'blob') {
- commit(types.TOGGLE_FILE_OPEN, file.path);
- commit(types.ADD_FILE_TO_CHANGED, file.path);
- dispatch('setFileActive', file.path);
- dispatch('triggerFilesChange');
- dispatch('burstUnusedSeal');
- }
+ const data = decorateFiles({
+ data: [fullName],
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ type,
+ tempFile: true,
+ content,
+ base64,
+ binary,
+ rawPath,
+ });
+ const { file, parentPath } = data;
- if (parentPath && !state.entries[parentPath].opened) {
- commit(types.TOGGLE_TREE_OPEN, parentPath);
- }
+ commit(types.CREATE_TMP_ENTRY, {
+ data,
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ });
- resolve(file);
+ if (type === 'blob') {
+ commit(types.TOGGLE_FILE_OPEN, file.path);
+ commit(types.ADD_FILE_TO_CHANGED, file.path);
+ dispatch('setFileActive', file.path);
+ dispatch('triggerFilesChange');
+ dispatch('burstUnusedSeal');
+ }
- return null;
- });
+ if (parentPath && !state.entries[parentPath].opened) {
+ commit(types.TOGGLE_TREE_OPEN, parentPath);
+ }
+};
export const scrollToTab = () => {
Vue.nextTick(() => {
@@ -211,8 +206,9 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path];
const { prevPath, prevName, prevParentPath } = entry;
const isTree = entry.type === 'tree';
+ const prevEntry = prevPath && state.entries[prevPath];
- if (prevPath) {
+ if (prevPath && (!prevEntry || prevEntry.deleted)) {
dispatch('renameEntry', {
path,
name: prevName,
@@ -245,6 +241,11 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => {
const entry = state.entries[path];
const newPath = parentPath ? `${parentPath}/${name}` : name;
+ const existingParent = parentPath && state.entries[parentPath];
+
+ if (parentPath && (!existingParent || existingParent.deleted)) {
+ dispatch('createTempEntry', { name: parentPath, type: 'tree' });
+ }
commit(types.RENAME_ENTRY, { path, name, parentPath });
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 52bf9becd0f..e206f9bee9e 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -83,8 +83,11 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
-export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
+export const showEmptyState = ({ commit, state, dispatch }, { projectId, branchId }) => {
const treePath = `${projectId}/${branchId}`;
+
+ dispatch('setCurrentBranchId', branchId);
+
commit(types.CREATE_TREE, { treePath });
commit(types.TOGGLE_LOADING, {
entry: state.trees[treePath],
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
index 47bd70537f1..089dedd14cb 100644
--- a/app/assets/javascripts/pages/admin/application_settings/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -1,7 +1,11 @@
import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select';
+import selfMonitor from '~/self_monitor';
document.addEventListener('DOMContentLoaded', () => {
+ if (gon.features && gon.features.selfMonitoringProject) {
+ selfMonitor();
+ }
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
new file mode 100644
index 00000000000..2f364eae67f
--- /dev/null
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -0,0 +1,160 @@
+<script>
+import Vue from 'vue';
+import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { __, s__, sprintf } from '~/locale';
+import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
+
+Vue.use(GlToast);
+
+export default {
+ components: {
+ GlFormGroup,
+ GlButton,
+ GlModal,
+ GlToggle,
+ },
+ formLabels: {
+ createProject: __('Create Project'),
+ },
+ data() {
+ return {
+ modalId: 'delete-self-monitor-modal',
+ };
+ },
+ computed: {
+ ...mapState('selfMonitoring', [
+ 'projectEnabled',
+ 'projectCreated',
+ 'showAlert',
+ 'projectPath',
+ 'loading',
+ 'alertContent',
+ ]),
+ selfMonitorEnabled: {
+ get() {
+ return this.projectEnabled;
+ },
+ set(projectEnabled) {
+ this.setSelfMonitor(projectEnabled);
+ },
+ },
+ selfMonitorProjectFullUrl() {
+ return `${getBaseURL()}/${this.projectPath}`;
+ },
+ selfMonitoringFormText() {
+ if (this.projectCreated) {
+ return sprintf(
+ s__(
+ 'SelfMonitoring|Enabling this feature creates a %{projectLinkStart}project%{projectLinkEnd} that can be used to monitor the health of your instance.',
+ ),
+ {
+ projectLinkStart: `<a href="${this.selfMonitorProjectFullUrl}">`,
+ projectLinkEnd: '</a>',
+ },
+ false,
+ );
+ }
+
+ return s__(
+ 'SelfMonitoring|Enabling this feature creates a project that can be used to monitor the health of your instance.',
+ );
+ },
+ },
+ watch: {
+ selfMonitorEnabled() {
+ this.saveChangesSelfMonitorProject();
+ },
+ showAlert() {
+ let toastOptions = {
+ onComplete: () => {
+ this.resetAlert();
+ },
+ };
+
+ if (this.showAlert) {
+ if (this.alertContent.actionName && this.alertContent.actionName.length > 0) {
+ toastOptions = {
+ ...toastOptions,
+ action: {
+ text: this.alertContent.actionText,
+ onClick: (_, toastObject) => {
+ this[this.alertContent.actionName]();
+ toastObject.goAway(0);
+ },
+ },
+ };
+ }
+ this.$toast.show(this.alertContent.message, toastOptions);
+ }
+ },
+ },
+ methods: {
+ ...mapActions('selfMonitoring', [
+ 'setSelfMonitor',
+ 'createProject',
+ 'deleteProject',
+ 'resetAlert',
+ ]),
+ hideSelfMonitorModal() {
+ this.$root.$emit('bv::hide::modal', this.modalId);
+ this.setSelfMonitor(true);
+ },
+ showSelfMonitorModal() {
+ this.$root.$emit('bv::show::modal', this.modalId);
+ },
+ saveChangesSelfMonitorProject() {
+ if (this.projectCreated && !this.projectEnabled) {
+ this.showSelfMonitorModal();
+ } else {
+ this.createProject();
+ }
+ },
+ viewSelfMonitorProject() {
+ visitUrl(this.selfMonitorProjectFullUrl);
+ },
+ },
+};
+</script>
+<template>
+ <section class="settings no-animate js-self-monitoring-settings">
+ <div class="settings-header">
+ <h4 class="js-section-header">
+ {{ s__('SelfMonitoring|Self monitoring') }}
+ </h4>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
+ <p class="js-section-sub-header">
+ {{ s__('SelfMonitoring|Enable or disable instance self monitoring') }}
+ </p>
+ </div>
+ <div class="settings-content">
+ <form name="self-monitoring-form">
+ <p v-html="selfMonitoringFormText"></p>
+ <gl-form-group :label="$options.formLabels.createProject" label-for="self-monitor-toggle">
+ <gl-toggle
+ v-model="selfMonitorEnabled"
+ :is-loading="loading"
+ name="self-monitor-toggle"
+ />
+ </gl-form-group>
+ </form>
+ </div>
+ <gl-modal
+ :title="s__('SelfMonitoring|Disable self monitoring?')"
+ :modal-id="modalId"
+ :ok-title="__('Delete project')"
+ :cancel-title="__('Cancel')"
+ ok-variant="danger"
+ @ok="deleteProject"
+ @cancel="hideSelfMonitorModal"
+ >
+ <div>
+ {{
+ s__(
+ 'SelfMonitoring|Disabling this feature will delete the self monitoring project. Are you sure you want to delete the project?',
+ )
+ }}
+ </div>
+ </gl-modal>
+ </section>
+</template>
diff --git a/app/assets/javascripts/self_monitor/index.js b/app/assets/javascripts/self_monitor/index.js
new file mode 100644
index 00000000000..42c94e11989
--- /dev/null
+++ b/app/assets/javascripts/self_monitor/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import store from './store';
+import SelfMonitorForm from './components/self_monitor_form.vue';
+
+export default () => {
+ const el = document.querySelector('.js-self-monitoring-settings');
+ let selfMonitorProjectCreated;
+
+ if (el) {
+ selfMonitorProjectCreated = el.dataset.selfMonitoringProjectExists;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ store: store({
+ projectEnabled: selfMonitorProjectCreated,
+ ...el.dataset,
+ }),
+ render(createElement) {
+ return createElement(SelfMonitorForm);
+ },
+ });
+ }
+};
diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js
new file mode 100644
index 00000000000..f8430a9b136
--- /dev/null
+++ b/app/assets/javascripts/self_monitor/store/actions.js
@@ -0,0 +1,126 @@
+import { __, s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { backOff } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
+
+const TWO_MINUTES = 120000;
+
+function backOffRequest(makeRequestCallback) {
+ return backOff((next, stop) => {
+ makeRequestCallback()
+ .then(resp => {
+ if (resp.status === statusCodes.ACCEPTED) {
+ next();
+ } else {
+ stop(resp);
+ }
+ })
+ .catch(stop);
+ }, TWO_MINUTES);
+}
+
+export const setSelfMonitor = ({ commit }, enabled) => commit(types.SET_ENABLED, enabled);
+
+export const createProject = ({ dispatch }) => dispatch('requestCreateProject');
+
+export const resetAlert = ({ commit }) => commit(types.SET_SHOW_ALERT, false);
+
+export const requestCreateProject = ({ dispatch, state, commit }) => {
+ commit(types.SET_LOADING, true);
+ axios
+ .post(state.createProjectEndpoint)
+ .then(resp => {
+ if (resp.status === statusCodes.ACCEPTED) {
+ dispatch('requestCreateProjectStatus', resp.data.job_id);
+ }
+ })
+ .catch(error => {
+ dispatch('requestCreateProjectError', error);
+ });
+};
+
+export const requestCreateProjectStatus = ({ dispatch, state }, jobId) => {
+ backOffRequest(() => axios.get(state.createProjectStatusEndpoint, { params: { job_id: jobId } }))
+ .then(resp => {
+ if (resp.status === statusCodes.OK) {
+ dispatch('requestCreateProjectSuccess', resp.data);
+ }
+ })
+ .catch(error => {
+ dispatch('requestCreateProjectError', error);
+ });
+};
+
+export const requestCreateProjectSuccess = ({ commit }, selfMonitorData) => {
+ commit(types.SET_LOADING, false);
+ commit(types.SET_PROJECT_URL, selfMonitorData.project_full_path);
+ commit(types.SET_ALERT_CONTENT, {
+ message: s__('SelfMonitoring|Self monitoring project has been successfully created.'),
+ actionText: __('View project'),
+ actionName: 'viewSelfMonitorProject',
+ });
+ commit(types.SET_SHOW_ALERT, true);
+ commit(types.SET_PROJECT_CREATED, true);
+};
+
+export const requestCreateProjectError = ({ commit }, error) => {
+ const { response } = error;
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ commit(types.SET_ALERT_CONTENT, {
+ message: `${__('There was an error saving your changes.')} ${message}`,
+ });
+ commit(types.SET_SHOW_ALERT, true);
+ commit(types.SET_LOADING, false);
+};
+
+export const deleteProject = ({ dispatch }) => dispatch('requestDeleteProject');
+
+export const requestDeleteProject = ({ dispatch, state, commit }) => {
+ commit(types.SET_LOADING, true);
+ axios
+ .delete(state.deleteProjectEndpoint)
+ .then(resp => {
+ if (resp.status === statusCodes.ACCEPTED) {
+ dispatch('requestDeleteProjectStatus', resp.data.job_id);
+ }
+ })
+ .catch(error => {
+ dispatch('requestDeleteProjectError', error);
+ });
+};
+
+export const requestDeleteProjectStatus = ({ dispatch, state }, jobId) => {
+ backOffRequest(() => axios.get(state.deleteProjectStatusEndpoint, { params: { job_id: jobId } }))
+ .then(resp => {
+ if (resp.status === statusCodes.OK) {
+ dispatch('requestDeleteProjectSuccess', resp.data);
+ }
+ })
+ .catch(error => {
+ dispatch('requestDeleteProjectError', error);
+ });
+};
+
+export const requestDeleteProjectSuccess = ({ commit }) => {
+ commit(types.SET_PROJECT_URL, '');
+ commit(types.SET_PROJECT_CREATED, false);
+ commit(types.SET_ALERT_CONTENT, {
+ message: s__('SelfMonitoring|Self monitoring project has been successfully deleted.'),
+ actionText: __('Undo'),
+ actionName: 'createProject',
+ });
+ commit(types.SET_SHOW_ALERT, true);
+ commit(types.SET_LOADING, false);
+};
+
+export const requestDeleteProjectError = ({ commit }, error) => {
+ const { response } = error;
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ commit(types.SET_ALERT_CONTENT, {
+ message: `${__('There was an error saving your changes.')} ${message}`,
+ });
+ commit(types.SET_LOADING, false);
+};
diff --git a/app/assets/javascripts/self_monitor/store/index.js b/app/assets/javascripts/self_monitor/store/index.js
new file mode 100644
index 00000000000..a222e9c87b8
--- /dev/null
+++ b/app/assets/javascripts/self_monitor/store/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ modules: {
+ selfMonitoring: {
+ namespaced: true,
+ state: createState(initialState),
+ actions,
+ mutations,
+ },
+ },
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/self_monitor/store/mutation_types.js b/app/assets/javascripts/self_monitor/store/mutation_types.js
new file mode 100644
index 00000000000..c5952b66144
--- /dev/null
+++ b/app/assets/javascripts/self_monitor/store/mutation_types.js
@@ -0,0 +1,6 @@
+export const SET_ENABLED = 'SET_ENABLED';
+export const SET_PROJECT_CREATED = 'SET_PROJECT_CREATED';
+export const SET_SHOW_ALERT = 'SET_SHOW_ALERT';
+export const SET_PROJECT_URL = 'SET_PROJECT_URL';
+export const SET_LOADING = 'SET_LOADING';
+export const SET_ALERT_CONTENT = 'SET_ALERT_CONTENT';
diff --git a/app/assets/javascripts/self_monitor/store/mutations.js b/app/assets/javascripts/self_monitor/store/mutations.js
new file mode 100644
index 00000000000..7dca8bcdc4d
--- /dev/null
+++ b/app/assets/javascripts/self_monitor/store/mutations.js
@@ -0,0 +1,22 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_ENABLED](state, enabled) {
+ state.projectEnabled = enabled;
+ },
+ [types.SET_PROJECT_CREATED](state, created) {
+ state.projectCreated = created;
+ },
+ [types.SET_SHOW_ALERT](state, show) {
+ state.showAlert = show;
+ },
+ [types.SET_PROJECT_URL](state, url) {
+ state.projectPath = url;
+ },
+ [types.SET_LOADING](state, loading) {
+ state.loading = loading;
+ },
+ [types.SET_ALERT_CONTENT](state, content) {
+ state.alertContent = content;
+ },
+};
diff --git a/app/assets/javascripts/self_monitor/store/state.js b/app/assets/javascripts/self_monitor/store/state.js
new file mode 100644
index 00000000000..b8b4a4af614
--- /dev/null
+++ b/app/assets/javascripts/self_monitor/store/state.js
@@ -0,0 +1,15 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+
+export default (initialState = {}) => ({
+ projectEnabled: parseBoolean(initialState.projectEnabled) || false,
+ projectCreated: parseBoolean(initialState.selfMonitorProjectCreated) || false,
+ createProjectEndpoint: initialState.createSelfMonitoringProjectPath || '',
+ deleteProjectEndpoint: initialState.deleteSelfMonitoringProjectPath || '',
+ createProjectStatusEndpoint: initialState.statusCreateSelfMonitoringProjectPath || '',
+ deleteProjectStatusEndpoint: initialState.statusDeleteSelfMonitoringProjectPath || '',
+ selfMonitorProjectPath: initialState.selfMonitoringProjectFullPath || '',
+ showAlert: false,
+ projectPath: '',
+ loading: false,
+ alertContent: {},
+});
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 657e5accdab..719de095faf 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -281,7 +281,7 @@ module MarkupHelper
context.reverse_merge!(
current_user: (current_user if defined?(current_user)),
- # RelativeLinkFilter
+ # RepositoryLinkFilter and UploadLinkFilter
commit: @commit,
project_wiki: @project_wiki,
ref: @ref,
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 55a48da8342..ff40d7da892 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -47,6 +47,9 @@
.settings-content
= render 'performance_bar'
+- if Feature.enabled?(:self_monitoring_project)
+ .js-self-monitoring-settings{ data: self_monitoring_project_data }
+
%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header#usage-statistics
%h4
diff --git a/changelogs/unreleased/195776-fix-discard-rename-web-ide.yml b/changelogs/unreleased/195776-fix-discard-rename-web-ide.yml
new file mode 100644
index 00000000000..5780a07a047
--- /dev/null
+++ b/changelogs/unreleased/195776-fix-discard-rename-web-ide.yml
@@ -0,0 +1,5 @@
+---
+title: Fix discarding renamed directories in Web IDE
+merge_request: 22943
+author:
+type: fixed
diff --git a/changelogs/unreleased/34522-webide-empty-repos.yml b/changelogs/unreleased/34522-webide-empty-repos.yml
new file mode 100644
index 00000000000..3fbd097dba8
--- /dev/null
+++ b/changelogs/unreleased/34522-webide-empty-repos.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix: WebIDE doesn''t work on empty repositories again'
+merge_request: 22950
+author:
+type: fixed
diff --git a/changelogs/unreleased/55347-mr-diffs-file-count-increments-while-batch-loading.yml b/changelogs/unreleased/55347-mr-diffs-file-count-increments-while-batch-loading.yml
new file mode 100644
index 00000000000..3058624a4d8
--- /dev/null
+++ b/changelogs/unreleased/55347-mr-diffs-file-count-increments-while-batch-loading.yml
@@ -0,0 +1,5 @@
+---
+title: Fix MR diffs file count increments while batch loading
+merge_request: 21764
+author:
+type: fixed
diff --git a/changelogs/unreleased/7225-no-audit-event-for-adding-a-user-to-a-group-or-project-through-api.yml b/changelogs/unreleased/7225-no-audit-event-for-adding-a-user-to-a-group-or-project-through-api.yml
new file mode 100644
index 00000000000..c139cea868a
--- /dev/null
+++ b/changelogs/unreleased/7225-no-audit-event-for-adding-a-user-to-a-group-or-project-through-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add audit events to the adding members to project or group API endpoint
+merge_request: 21633
+author:
+type: changed
diff --git a/changelogs/unreleased/ab-projects-api-more-indexes.yml b/changelogs/unreleased/ab-projects-api-more-indexes.yml
new file mode 100644
index 00000000000..1567e78cba3
--- /dev/null
+++ b/changelogs/unreleased/ab-projects-api-more-indexes.yml
@@ -0,0 +1,5 @@
+---
+title: Add more indexes for other order_by options (Projects API)
+merge_request: 22784
+author:
+type: performance
diff --git a/changelogs/unreleased/add-geo-node-api.yml b/changelogs/unreleased/add-geo-node-api.yml
new file mode 100644
index 00000000000..9db482531cc
--- /dev/null
+++ b/changelogs/unreleased/add-geo-node-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint for creating a Geo node
+merge_request: 22392
+author: Rajendra Kadam
+type: added
diff --git a/changelogs/unreleased/dblessing_update_net_ldap_gem.yml b/changelogs/unreleased/dblessing_update_net_ldap_gem.yml
new file mode 100644
index 00000000000..52cced0ef6c
--- /dev/null
+++ b/changelogs/unreleased/dblessing_update_net_ldap_gem.yml
@@ -0,0 +1,5 @@
+---
+title: Update the Net-LDAP gem to 0.16.2
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/split-up-relativelinkfilter.yml b/changelogs/unreleased/split-up-relativelinkfilter.yml
new file mode 100644
index 00000000000..feaa9f290ab
--- /dev/null
+++ b/changelogs/unreleased/split-up-relativelinkfilter.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid making Gitaly calls when some Markdown text links to an uploaded file
+merge_request: 22631
+author:
+type: performance
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 3e017b810b6..4698a42c8b6 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -208,7 +208,7 @@ class Gitlab::Seeder::CycleAnalytics
job = merge_request.head_pipeline.builds.where.not(environment: nil).last
job.success!
- pipeline.update_status
+ job.pipeline.update_status
end
end
end
diff --git a/db/migrate/20200110144316_add_indexes_for_projects_api.rb b/db/migrate/20200110144316_add_indexes_for_projects_api.rb
new file mode 100644
index 00000000000..6b0ca252456
--- /dev/null
+++ b/db/migrate/20200110144316_add_indexes_for_projects_api.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class AddIndexesForProjectsApi < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ COLUMNS = %i(created_at last_activity_at updated_at name path)
+
+ def up
+ COLUMNS.each do |column|
+ add_concurrent_index :projects, [column, :id], where: 'visibility_level = 20', order: { id: :desc }, name: "index_projects_api_vis20_#{column}_id_desc"
+ add_concurrent_index :projects, [column, :id], where: 'visibility_level = 20', name: "index_projects_api_vis20_#{column}"
+ end
+
+ remove_concurrent_index_by_name :projects, 'index_projects_on_visibility_level_created_at_id_desc'
+ remove_concurrent_index_by_name :projects, 'index_projects_on_visibility_level_created_at_desc_id_desc'
+ end
+
+ def down
+ add_concurrent_index :projects, %i(visibility_level created_at id), order: { id: :desc }, name: 'index_projects_on_visibility_level_created_at_id_desc'
+ add_concurrent_index :projects, %i(visibility_level created_at id), order: { created_at: :desc, id: :desc }, name: 'index_projects_on_visibility_level_created_at_desc_id_desc'
+
+ COLUMNS.each do |column|
+ remove_concurrent_index_by_name :projects, "index_projects_api_vis20_#{column}_id_desc"
+ remove_concurrent_index_by_name :projects, "index_projects_api_vis20_#{column}"
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7f1a7ac0ff1..c2c5bb43c5e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_01_08_233040) do
+ActiveRecord::Schema.define(version: 2020_01_10_144316) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -3353,6 +3353,8 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do
t.boolean "autoclose_referenced_issues"
t.string "suggestion_commit_message", limit: 255
t.index "lower((name)::text)", name: "index_projects_on_lower_name"
+ t.index ["created_at", "id"], name: "index_projects_api_vis20_created_at", where: "(visibility_level = 20)"
+ t.index ["created_at", "id"], name: "index_projects_api_vis20_created_at_id_desc", order: { id: :desc }, where: "(visibility_level = 20)"
t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id"
t.index ["creator_id"], name: "index_projects_on_creator_id"
t.index ["description"], name: "index_projects_on_description_trigram", opclass: :gin_trgm_ops, using: :gin
@@ -3360,6 +3362,8 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do
t.index ["id"], name: "index_on_id_partial_with_legacy_storage", where: "((storage_version < 2) OR (storage_version IS NULL))"
t.index ["id"], name: "index_projects_on_id_partial_for_visibility", unique: true, where: "(visibility_level = ANY (ARRAY[10, 20]))"
t.index ["id"], name: "index_projects_on_mirror_and_mirror_trigger_builds_both_true", where: "((mirror IS TRUE) AND (mirror_trigger_builds IS TRUE))"
+ t.index ["last_activity_at", "id"], name: "index_projects_api_vis20_last_activity_at", where: "(visibility_level = 20)"
+ t.index ["last_activity_at", "id"], name: "index_projects_api_vis20_last_activity_at_id_desc", order: { id: :desc }, where: "(visibility_level = 20)"
t.index ["last_activity_at"], name: "index_projects_on_last_activity_at"
t.index ["last_repository_check_at"], name: "index_projects_on_last_repository_check_at", where: "(last_repository_check_at IS NOT NULL)"
t.index ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed"
@@ -3368,8 +3372,12 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do
t.index ["marked_for_deletion_by_user_id"], name: "index_projects_on_marked_for_deletion_by_user_id", where: "(marked_for_deletion_by_user_id IS NOT NULL)"
t.index ["mirror_last_successful_update_at"], name: "index_projects_on_mirror_last_successful_update_at"
t.index ["mirror_user_id"], name: "index_projects_on_mirror_user_id"
+ t.index ["name", "id"], name: "index_projects_api_vis20_name", where: "(visibility_level = 20)"
+ t.index ["name", "id"], name: "index_projects_api_vis20_name_id_desc", order: { id: :desc }, where: "(visibility_level = 20)"
t.index ["name"], name: "index_projects_on_name_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["namespace_id"], name: "index_projects_on_namespace_id"
+ t.index ["path", "id"], name: "index_projects_api_vis20_path", where: "(visibility_level = 20)"
+ t.index ["path", "id"], name: "index_projects_api_vis20_path_id_desc", order: { id: :desc }, where: "(visibility_level = 20)"
t.index ["path"], name: "index_projects_on_path"
t.index ["path"], name: "index_projects_on_path_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["pending_delete"], name: "index_projects_on_pending_delete"
@@ -3379,8 +3387,8 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do
t.index ["runners_token"], name: "index_projects_on_runners_token"
t.index ["runners_token_encrypted"], name: "index_projects_on_runners_token_encrypted"
t.index ["star_count"], name: "index_projects_on_star_count"
- t.index ["visibility_level", "created_at", "id"], name: "index_projects_on_visibility_level_created_at_desc_id_desc", order: { created_at: :desc, id: :desc }
- t.index ["visibility_level", "created_at", "id"], name: "index_projects_on_visibility_level_created_at_id_desc", order: { id: :desc }
+ t.index ["updated_at", "id"], name: "index_projects_api_vis20_updated_at", where: "(visibility_level = 20)"
+ t.index ["updated_at", "id"], name: "index_projects_api_vis20_updated_at_id_desc", order: { id: :desc }, where: "(visibility_level = 20)"
end
create_table "prometheus_alert_events", force: :cascade do |t|
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 0fbdaa942e8..771efc2c8c6 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -298,7 +298,7 @@ log data to build up in `pg_xlog`. Removing the unused slots can reduce the amou
1. Start a PostgreSQL console session:
```sh
- sudo gitlab-psql gitlabhq_production
+ sudo gitlab-psql
```
Note: **Note:** Using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index e44d69f1419..2786d00ebbd 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -3,6 +3,54 @@
In order to interact with Geo node endpoints, you need to authenticate yourself
as an admin.
+## Create a new Geo node
+
+Creates a new Geo node.
+
+```
+POST /geo_nodes
+```
+
+| Attribute | Type | Required | Description |
+| ----------------------------| ------- | -------- | -----------------------------------------------------------------|
+| `primary` | boolean | no | Specifying whether this node will be primary. Defaults to false. |
+| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. Defaults to true. |
+| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in `gitlab.rb`, otherwise it must match `external_url` |
+| `url` | string | yes | The user-facing URL for the Geo node. |
+| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set. |
+| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. Defaults to 10. |
+| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. Defaults to 25. |
+| `verification_max_capacity` | integer | no | Control the maximum concurrency of repository verification for this node. Defaults to 100. |
+| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. Defaults to 10. |
+| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. Defaults to false. |
+
+Example response:
+
+```json
+{
+ "id": 3,
+ "name": "Test Node 1",
+ "url": "https://secondary.example.com/",
+ "internal_url": "https://secondary.example.com/",
+ "primary": false,
+ "enabled": true,
+ "current": false,
+ "files_max_capacity": 10,
+ "repos_max_capacity": 25,
+ "verification_max_capacity": 100,
+ "container_repositories_max_capacity": 10,
+ "sync_object_storage": false,
+ "clone_protocol": "http",
+ "web_edit_url": "https://primary.example.com/admin/geo/nodes/3/edit",
+ "web_geo_projects_url": "http://secondary.example.com/admin/geo/projects",
+ "_links": {
+ "self": "https://primary.example.com/api/v4/geo_nodes/3",
+ "status": "https://primary.example.com/api/v4/geo_nodes/3/status",
+ "repair": "https://primary.example.com/api/v4/geo_nodes/3/repair"
+ }
+}
+```
+
## Retrieve configuration about all Geo nodes
```
diff --git a/doc/development/packages.md b/doc/development/packages.md
index 4ea71afac80..980c1869a0a 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -21,6 +21,7 @@ The goal of the Package group is to build a set of features that, within three y
| Format | Use case |
| ------ | ------ |
| [Bower](https://gitlab.com/gitlab-org/gitlab/issues/36888) | Boost your front end development by hosting your own Bower components. |
+| [Cargo](https://gitlab.com/gitlab-org/gitlab/issues/33060) | Cargo is the Rust package manager. Build, publish and share Rust packages |
| [Chef](https://gitlab.com/gitlab-org/gitlab/issues/36889) | Configuration management with Chef using all the benefits of a repository manager. |
| [CocoaPods](https://gitlab.com/gitlab-org/gitlab/issues/36890) | Speed up development with Xcode and CocoaPods. |
| [Conda](https://gitlab.com/gitlab-org/gitlab/issues/36891) | Secure and private local Conda repositories. |
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 5693c6c50ec..2672b0f3461 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -199,9 +199,60 @@ include:
- template: SAST.gitlab-ci.yml
variables:
+ SAST_DISABLE_DIND: "true"
SCAN_KUBERNETES_MANIFESTS: "true"
```
+#### Pre-compilation
+
+If your project requires custom build configurations, it can be preferable to avoid
+compilation during your SAST execution and instead pass all job artifacts from an
+earlier stage within the pipeline.
+
+To pass your project's dependencies as artifacts, the dependencies must be included
+in the project's working directory and specified using the `artifacts:path` configuration.
+If all dependencies are present, the `-compile=false` flag can be provided to the
+analyzer and compilation will be skipped:
+
+```yaml
+image: maven:3.6-jdk-8-alpine
+
+stages:
+ - build
+ - test
+
+include:
+ template: SAST.gitlab-ci.yml
+
+variables:
+ SAST_DISABLE_DIND: "true"
+
+build:
+ stage: build
+ script:
+ - mvn package -Dmaven.repo.local=./.m2/repository
+ artifacts:
+ paths:
+ - .m2/
+ - target/
+
+spotbugs-sast:
+ dependencies: build
+ script:
+ - /analyzer run -compile=false
+ variables:
+ MAVEN_REPO_PATH: ./.m2/repository
+ artifacts:
+ reports:
+ sast: gl-sast-report.json
+```
+
+NOTE: **Note:**
+The path to the vendored directory must be specified explicitly to allow
+the analyzer to recognize the compiled artifacts. This configuration can vary per
+analyzer but in the case of Java above, `MAVEN_REPO_PATH` can be used.
+See [Analyzer settings](#analyzer-settings) for the complete list of available options.
+
### Available variables
SAST can be [configured](#customizing-the-sast-settings) using environment variables.
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 6b65aaba2c5..5fdbbcedfc9 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -73,20 +73,20 @@ If you have 2FA enabled, you need to use a [personal access token](../../profile
### Authenticating with an OAuth token
To authenticate with an [OAuth token](../../../api/oauth2.md#resource-owner-password-credentials-flow)
-or [personal access token](../../profile/personal_access_tokens.md), add a corresponding section to your `.npmrc` file:
+or [personal access token](../../profile/personal_access_tokens.md), set your NPM configuration:
-```ini
-; Set URL for your scoped packages.
-; For example package with name `@foo/bar` will use this URL for download
-@foo:registry=https://gitlab.com/api/v4/packages/npm/
+```bash
+# Set URL for your scoped packages.
+# For example package with name `@foo/bar` will use this URL for download
+npm config set @foo:registry https://gitlab.com/api/v4/packages/npm/
-; Add the token for the scoped packages URL. This will allow you to download
-; `@foo/` packages from private projects.
-//gitlab.com/api/v4/packages/npm/:_authToken=<your_token>
+# Add the token for the scoped packages URL. This will allow you to download
+# `@foo/` packages from private projects.
+npm config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
-; Add token for uploading to the registry. Replace <your_project_id>
-; with the project you want your package to be uploaded to.
-//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken=<your_token>
+# Add token for uploading to the registry. Replace <your_project_id>
+# with the project you want your package to be uploaded to.
+npm config set '//gitlab.com/api/v4/packages/npm/:_authToken' "<your_token>"
```
Replace `<your_project_id>` with your project ID which can be found on the home page
@@ -103,13 +103,11 @@ If you encounter an error message with [Yarn](https://yarnpkg.com/en/), see the
### Using variables to avoid hard-coding auth token values
-To avoid hard-coding the `authToken` value, you may use a variables in its place.
-In your `.npmrc` file, you would add:
+To avoid hard-coding the `authToken` value, you may use a variables in its place:
-```ini
-@foo:registry=https://gitlab.com/api/v4/packages/npm/
-//gitlab.com/api/v4/packages/npm/:_authToken=${NPM_TOKEN}
-//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken=${NPM_TOKEN}
+```bash
+npm config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "${NPM_TOKEN}"
+npm config set '//gitlab.com/api/v4/packages/npm/:_authToken' "${NPM_TOKEN}"
```
Then, you could run `npm publish` either locally or via GitLab CI/CD:
@@ -227,6 +225,14 @@ And the `.npmrc` file should look like:
@foo:registry=https://gitlab.com/api/v4/packages/npm/
```
+### `npm install` returns `Error: Failed to replace env in config: ${NPM_TOKEN}`
+
+You do not need a token to run `npm install` unless your project is private (the token is only required to publish). If the `.npmrc` file was checked in with a reference to `$NPM_TOKEN`, you can remove it. If you prefer to leave the reference in, you'll need to set a value prior to running `npm install` or set the value using [GitLab environment variables](./../../../ci/variables/README.md):
+
+```bash
+NPM_TOKEN=<your_token> npm install
+```
+
## NPM dependencies metadata
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11867) in GitLab Premium 12.6.
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
index 3ccae8d85cc..d06c59907b4 100644
--- a/lib/api/helpers/members_helpers.rb
+++ b/lib/api/helpers/members_helpers.rb
@@ -41,6 +41,10 @@ module API
GroupMembersFinder.new(group).execute
end
+ def create_member(current_user, user, source, params)
+ source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+ end
+
def present_members(members)
present members, with: Entities::Member, current_user: current_user
end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index e745bd0d4a9..e4df2f341c6 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -101,12 +101,12 @@ module API
user = User.find_by_id(params[:user_id])
not_found!('User') unless user
- member = source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+ member = create_member(current_user, user, source, params)
if !member
not_allowed! # This currently can only be reached in EE
elsif member.persisted? && member.valid?
- present_members member
+ present_members(member)
else
render_validation_error!(member)
end
diff --git a/lib/banzai/filter/base_relative_link_filter.rb b/lib/banzai/filter/base_relative_link_filter.rb
new file mode 100644
index 00000000000..eca105ce9d9
--- /dev/null
+++ b/lib/banzai/filter/base_relative_link_filter.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'uri'
+
+module Banzai
+ module Filter
+ class BaseRelativeLinkFilter < HTML::Pipeline::Filter
+ include Gitlab::Utils::StrongMemoize
+
+ protected
+
+ def linkable_attributes
+ strong_memoize(:linkable_attributes) do
+ attrs = []
+
+ attrs += doc.search('a:not(.gfm)').map do |el|
+ el.attribute('href')
+ end
+
+ attrs += doc.search('img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
+ [el.attribute('src'), el.attribute('data-src')]
+ end
+
+ attrs.reject do |attr|
+ attr.blank? || attr.value.start_with?('//')
+ end
+ end
+ end
+
+ def relative_url_root
+ Gitlab.config.gitlab.relative_url_root.presence || '/'
+ end
+
+ def project
+ context[:project]
+ end
+
+ private
+
+ def unescape_and_scrub_uri(uri)
+ Addressable::URI.unescape(uri).scrub
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/repository_link_filter.rb
index 4f257189f8e..14cd607cc50 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/repository_link_filter.rb
@@ -4,19 +4,17 @@ require 'uri'
module Banzai
module Filter
- # HTML filter that "fixes" relative links to uploads or files in a repository.
+ # HTML filter that "fixes" relative links to files in a repository.
#
# Context options:
# :commit
- # :group
# :current_user
# :project
# :project_wiki
# :ref
# :requested_path
- class RelativeLinkFilter < HTML::Pipeline::Filter
- include Gitlab::Utils::StrongMemoize
-
+ # :system_note
+ class RepositoryLinkFilter < BaseRelativeLinkFilter
def call
return doc if context[:system_note]
@@ -26,7 +24,9 @@ module Banzai
load_uri_types
linkable_attributes.each do |attr|
- process_link_attr(attr)
+ if linkable_files? && repo_visible_to_user?
+ process_link_to_repository_attr(attr)
+ end
end
doc
@@ -35,8 +35,8 @@ module Banzai
protected
def load_uri_types
- return unless linkable_files?
return unless linkable_attributes.present?
+ return unless linkable_files?
return {} unless repository
@uri_types = request_path.present? ? get_uri_types([request_path]) : {}
@@ -57,24 +57,6 @@ module Banzai
end
end
- def linkable_attributes
- strong_memoize(:linkable_attributes) do
- attrs = []
-
- attrs += doc.search('a:not(.gfm)').map do |el|
- el.attribute('href')
- end
-
- attrs += doc.search('img, video, audio').flat_map do |el|
- [el.attribute('src'), el.attribute('data-src')]
- end
-
- attrs.reject do |attr|
- attr.blank? || attr.value.start_with?('//')
- end
- end
- end
-
def get_uri_types(paths)
return {} if paths.empty?
@@ -107,39 +89,6 @@ module Banzai
rescue URI::Error, Addressable::URI::InvalidURIError
end
- def process_link_attr(html_attr)
- if html_attr.value.start_with?('/uploads/')
- process_link_to_upload_attr(html_attr)
- elsif linkable_files? && repo_visible_to_user?
- process_link_to_repository_attr(html_attr)
- end
- end
-
- def process_link_to_upload_attr(html_attr)
- path_parts = [unescape_and_scrub_uri(html_attr.value)]
-
- if project
- path_parts.unshift(relative_url_root, project.full_path)
- elsif group
- path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
- else
- path_parts.unshift(relative_url_root)
- end
-
- begin
- path = Addressable::URI.escape(File.join(*path_parts))
- rescue Addressable::URI::InvalidURIError
- return
- end
-
- html_attr.value =
- if context[:only_path]
- path
- else
- Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
- end
- end
-
def process_link_to_repository_attr(html_attr)
uri = URI(html_attr.value)
@@ -239,10 +188,6 @@ module Banzai
@current_commit ||= context[:commit] || repository.commit(ref)
end
- def relative_url_root
- Gitlab.config.gitlab.relative_url_root.presence || '/'
- end
-
def repo_visible_to_user?
project && Ability.allowed?(current_user, :download_code, project)
end
@@ -251,14 +196,6 @@ module Banzai
context[:ref] || project.default_branch
end
- def group
- context[:group]
- end
-
- def project
- context[:project]
- end
-
def current_user
context[:current_user]
end
@@ -266,12 +203,6 @@ module Banzai
def repository
@repository ||= project&.repository
end
-
- private
-
- def unescape_and_scrub_uri(uri)
- Addressable::URI.unescape(uri).scrub
- end
end
end
end
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
new file mode 100644
index 00000000000..023c1288367
--- /dev/null
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML filter that "fixes" links to uploads.
+ #
+ # Context options:
+ # :group
+ # :only_path
+ # :project
+ # :system_note
+ class UploadLinkFilter < BaseRelativeLinkFilter
+ def call
+ return doc if context[:system_note]
+
+ linkable_attributes.each do |attr|
+ process_link_to_upload_attr(attr)
+ end
+
+ doc
+ end
+
+ protected
+
+ def process_link_to_upload_attr(html_attr)
+ return unless html_attr.value.start_with?('/uploads/')
+
+ path_parts = [unescape_and_scrub_uri(html_attr.value)]
+
+ if project
+ path_parts.unshift(relative_url_root, project.full_path)
+ elsif group
+ path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
+ else
+ path_parts.unshift(relative_url_root)
+ end
+
+ begin
+ path = Addressable::URI.escape(File.join(*path_parts))
+ rescue Addressable::URI::InvalidURIError
+ return
+ end
+
+ html_attr.value =
+ if context[:only_path]
+ path
+ else
+ Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
+ end
+
+ html_attr.parent.add_class('gfm')
+ end
+
+ def group
+ context[:group]
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index fe629a23ff1..5e02d972614 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -16,7 +16,10 @@ module Banzai
[
Filter::ReferenceRedactorFilter,
Filter::InlineMetricsRedactorFilter,
- Filter::RelativeLinkFilter,
+ # UploadLinkFilter must come before RepositoryLinkFilter to
+ # prevent unnecessary Gitaly calls from being made.
+ Filter::UploadLinkFilter,
+ Filter::RepositoryLinkFilter,
Filter::IssuableStateFilter,
Filter::SuggestionFilter
]
diff --git a/lib/banzai/pipeline/relative_link_pipeline.rb b/lib/banzai/pipeline/relative_link_pipeline.rb
deleted file mode 100644
index 88651892acc..00000000000
--- a/lib/banzai/pipeline/relative_link_pipeline.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Pipeline
- class RelativeLinkPipeline < BasePipeline
- def self.filters
- FilterArray[
- Filter::RelativeLinkFilter
- ]
- end
- end
- end
-end
diff --git a/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb b/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
index 40c109207a9..899f381e911 100644
--- a/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
+++ b/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
@@ -24,12 +24,14 @@ module Gitlab
fingerprints = []
Key.where(id: start_id..stop_id, fingerprint_sha256: nil).find_each do |regular_key|
- fingerprint = Base64.decode64(generate_ssh_public_key(regular_key.key))
-
- fingerprints << {
- id: regular_key.id,
- fingerprint_sha256: ActiveRecord::Base.connection.escape_bytea(fingerprint)
- }
+ if fingerprint = generate_ssh_public_key(regular_key.key)
+ bytea = ActiveRecord::Base.connection.escape_bytea(Base64.decode64(fingerprint))
+
+ fingerprints << {
+ id: regular_key.id,
+ fingerprint_sha256: bytea
+ }
+ end
end
Gitlab::Database.bulk_insert(TEMP_TABLE, fingerprints)
@@ -48,7 +50,7 @@ module Gitlab
private
def generate_ssh_public_key(regular_key)
- Gitlab::SSHPublicKey.new(regular_key).fingerprint("SHA256").gsub("SHA256:", "")
+ Gitlab::SSHPublicKey.new(regular_key).fingerprint("SHA256")&.gsub("SHA256:", "")
end
def execute(query)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 955a3cc306c..8d731ef8bd5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5217,6 +5217,9 @@ msgstr ""
msgid "Create New Domain"
msgstr ""
+msgid "Create Project"
+msgstr ""
+
msgid "Create a GitLab account first, and then connect it to your %{label} account."
msgstr ""
@@ -5849,6 +5852,9 @@ msgstr ""
msgid "Delete pipeline"
msgstr ""
+msgid "Delete project"
+msgstr ""
+
msgid "Delete snippet"
msgstr ""
@@ -7209,9 +7215,6 @@ msgstr ""
msgid "Error Tracking"
msgstr ""
-msgid "Error creating a new path"
-msgstr ""
-
msgid "Error creating epic"
msgstr ""
@@ -16412,6 +16415,30 @@ msgstr ""
msgid "Self-monitoring project was not deleted. Please check logs for any error messages"
msgstr ""
+msgid "SelfMonitoring|Disable self monitoring?"
+msgstr ""
+
+msgid "SelfMonitoring|Disabling this feature will delete the self monitoring project. Are you sure you want to delete the project?"
+msgstr ""
+
+msgid "SelfMonitoring|Enable or disable instance self monitoring"
+msgstr ""
+
+msgid "SelfMonitoring|Enabling this feature creates a %{projectLinkStart}project%{projectLinkEnd} that can be used to monitor the health of your instance."
+msgstr ""
+
+msgid "SelfMonitoring|Enabling this feature creates a project that can be used to monitor the health of your instance."
+msgstr ""
+
+msgid "SelfMonitoring|Self monitoring"
+msgstr ""
+
+msgid "SelfMonitoring|Self monitoring project has been successfully created."
+msgstr ""
+
+msgid "SelfMonitoring|Self monitoring project has been successfully deleted."
+msgstr ""
+
msgid "Send a separate email notification to Developers."
msgstr ""
@@ -18263,7 +18290,7 @@ msgstr ""
msgid "The merge request can now be merged."
msgstr ""
-msgid "The name %{entryName} is already taken in this directory."
+msgid "The name \"%{name}\" is already taken in this directory."
msgstr ""
msgid "The number of changes to be fetched from GitLab when cloning a repository. This can speed up Pipelines execution. Keep empty or set to 0 to disable shallow clone by default and make GitLab CI fetch all branches and tags each time."
@@ -20444,6 +20471,9 @@ msgstr ""
msgid "View previous app"
msgstr ""
+msgid "View project"
+msgstr ""
+
msgid "View project labels"
msgstr ""
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 602bfc64710..12d7c0a396e 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -18,7 +18,7 @@ module QA
view 'app/assets/javascripts/deploy_keys/components/key.vue' do
element :key
element :key_title
- element :key_fingerprint
+ element :key_md5_fingerprint
end
def add_key
@@ -33,17 +33,17 @@ module QA
fill_in 'deploy_key_key', with: key
end
- def find_fingerprint(title)
+ def find_md5_fingerprint(title)
within_project_deploy_keys do
find_element(:key, text: title)
- .find(element_selector_css(:key_fingerprint)).text
+ .find(element_selector_css(:key_md5_fingerprint)).text.delete_prefix('MD5:')
end
end
- def has_key?(title, fingerprint)
+ def has_key?(title, md5_fingerprint)
within_project_deploy_keys do
find_element(:key, text: title)
- .has_css?(element_selector_css(:key_fingerprint), text: fingerprint)
+ .has_css?(element_selector_css(:key_md5_fingerprint), text: "MD5:#{md5_fingerprint}")
end
end
@@ -53,12 +53,6 @@ module QA
end
end
- def key_fingerprint
- within_project_deploy_keys do
- find_element(:key_fingerprint).text
- end
- end
-
private
def within_project_deploy_keys
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index d1d2f302013..3f8aba78f44 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -61,7 +61,7 @@ module QA
end
# Click the select element again to close the dropdown
- click_element :protected_branch_select
+ click_element :"allowed_to_#{action}_select"
end
end
end
diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb
index 869e2a71e47..26355729dab 100644
--- a/qa/qa/resource/deploy_key.rb
+++ b/qa/qa/resource/deploy_key.rb
@@ -5,10 +5,10 @@ module QA
class DeployKey < Base
attr_accessor :title, :key
- attribute :fingerprint do
+ attribute :md5_fingerprint do
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |key|
- key.find_fingerprint(title)
+ key.find_md5_fingerprint(title)
end
end
end
diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb
index c140cb9ca62..22bdea424ca 100644
--- a/qa/qa/resource/ssh_key.rb
+++ b/qa/qa/resource/ssh_key.rb
@@ -7,7 +7,7 @@ module QA
attr_accessor :title
- def_delegators :key, :private_key, :public_key, :fingerprint
+ def_delegators :key, :private_key, :public_key, :md5_fingerprint
def key
@key ||= Runtime::Key::RSA.new
diff --git a/qa/qa/runtime/key/base.rb b/qa/qa/runtime/key/base.rb
index 1281eceaff0..72d1673438a 100644
--- a/qa/qa/runtime/key/base.rb
+++ b/qa/qa/runtime/key/base.rb
@@ -4,7 +4,7 @@ module QA
module Runtime
module Key
class Base
- attr_reader :name, :bits, :private_key, :public_key, :fingerprint
+ attr_reader :name, :bits, :private_key, :public_key, :md5_fingerprint
def initialize(name, bits)
@name = name
@@ -29,7 +29,7 @@ module QA
def populate_key_data(path)
@private_key = ::File.binread(path)
@public_key = ::File.binread("#{path}.pub")
- @fingerprint =
+ @md5_fingerprint =
`ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
index 474a7904fea..c3379d41ff2 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
@@ -13,7 +13,7 @@ module QA
end
expect(page).to have_content("Title: #{key_title}")
- expect(page).to have_content(key.fingerprint)
+ expect(page).to have_content(key.md5_fingerprint)
Page::Main::Menu.perform(&:click_settings_link)
Page::Profile::Menu.perform(&:click_ssh_keys)
@@ -23,7 +23,7 @@ module QA
end
expect(page).not_to have_content("Title: #{key_title}")
- expect(page).not_to have_content(key.fingerprint)
+ expect(page).not_to have_content(key.md5_fingerprint)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
index 9c964c726f1..89aba112407 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
@@ -15,11 +15,11 @@ module QA
resource.key = deploy_key_value
end
- expect(deploy_key.fingerprint).to eq key.fingerprint
+ expect(deploy_key.md5_fingerprint).to eq key.md5_fingerprint
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |keys|
- expect(keys).to have_key(deploy_key_title, key.fingerprint)
+ expect(keys).to have_key(deploy_key_title, key.md5_fingerprint)
end
end
end
diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb
index a45fa67ce9e..9ebd85acb81 100644
--- a/spec/features/markdown/markdown_spec.rb
+++ b/spec/features/markdown/markdown_spec.rb
@@ -208,6 +208,8 @@ describe 'GitLab Markdown', :aggregate_failures do
@group = @feat.group
end
+ let(:project) { @feat.project } # Shadow this so matchers can use it
+
context 'default pipeline' do
before do
@html = markdown(@feat.raw_markdown)
@@ -216,8 +218,12 @@ describe 'GitLab Markdown', :aggregate_failures do
it_behaves_like 'all pipelines'
it 'includes custom filters' do
- aggregate_failures 'RelativeLinkFilter' do
- expect(doc).to parse_relative_links
+ aggregate_failures 'UploadLinkFilter' do
+ expect(doc).to parse_upload_links
+ end
+
+ aggregate_failures 'RepositoryLinkFilter' do
+ expect(doc).to parse_repository_links
end
aggregate_failures 'EmojiFilter' do
@@ -277,8 +283,12 @@ describe 'GitLab Markdown', :aggregate_failures do
it_behaves_like 'all pipelines'
it 'includes custom filters' do
- aggregate_failures 'RelativeLinkFilter' do
- expect(doc).not_to parse_relative_links
+ aggregate_failures 'UploadLinkFilter' do
+ expect(doc).to parse_upload_links
+ end
+
+ aggregate_failures 'RepositoryLinkFilter' do
+ expect(doc).not_to parse_repository_links
end
aggregate_failures 'EmojiFilter' do
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index b19b45928d9..59795c835a2 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -111,7 +111,13 @@ Markdown should be usable inside a link. Let's try!
- [**text**](#link-strong)
- [`text`](#link-code)
-### RelativeLinkFilter
+### UploadLinkFilter
+
+Linking to an upload in this project should work:
+[Relative Upload Link](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
+![Relative Upload Image](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
+
+### RepositoryLinkFilter
Linking to a file relative to this project's repository should work.
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 82088182a06..7f47677f56c 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -28,6 +28,7 @@ describe('CompareVersions', () => {
propsData: {
mergeRequestDiffs: diffsMockData,
mergeRequestDiff: diffsMockData[0],
+ diffFilesLength: 0,
targetBranch,
...props,
},
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap
new file mode 100644
index 00000000000..1d0f0c024d6
--- /dev/null
+++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`self monitor component When the self monitor project has not been created default state to match the default snapshot 1`] = `
+<section
+ class="settings no-animate js-self-monitoring-settings"
+>
+ <div
+ class="settings-header"
+ >
+ <h4
+ class="js-section-header"
+ >
+
+ Self monitoring
+
+ </h4>
+
+ <gl-button-stub
+ class="js-settings-toggle"
+ >
+ Expand
+ </gl-button-stub>
+
+ <p
+ class="js-section-sub-header"
+ >
+
+ Enable or disable instance self monitoring
+
+ </p>
+ </div>
+
+ <div
+ class="settings-content"
+ >
+ <form
+ name="self-monitoring-form"
+ >
+ <p>
+ Enabling this feature creates a project that can be used to monitor the health of your instance.
+ </p>
+
+ <gl-form-group-stub
+ label="Create Project"
+ label-for="self-monitor-toggle"
+ >
+ <gl-toggle-stub
+ labeloff="Toggle Status: OFF"
+ labelon="Toggle Status: ON"
+ name="self-monitor-toggle"
+ />
+ </gl-form-group-stub>
+ </form>
+ </div>
+
+ <gl-modal-stub
+ cancel-title="Cancel"
+ modalclass=""
+ modalid="delete-self-monitor-modal"
+ ok-title="Delete project"
+ ok-variant="danger"
+ title="Disable self monitoring?"
+ titletag="h4"
+ >
+ <div>
+
+ Disabling this feature will delete the self monitoring project. Are you sure you want to delete the project?
+
+ </div>
+ </gl-modal-stub>
+</section>
+`;
diff --git a/spec/frontend/self_monitor/components/self_monitor_spec.js b/spec/frontend/self_monitor/components/self_monitor_spec.js
new file mode 100644
index 00000000000..b95c7514047
--- /dev/null
+++ b/spec/frontend/self_monitor/components/self_monitor_spec.js
@@ -0,0 +1,83 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
+import { createStore } from '~/self_monitor/store';
+
+describe('self monitor component', () => {
+ let wrapper;
+ let store;
+
+ describe('When the self monitor project has not been created', () => {
+ beforeEach(() => {
+ store = createStore({
+ projectEnabled: false,
+ selfMonitorProjectCreated: false,
+ createSelfMonitoringProjectPath: '/create',
+ deleteSelfMonitoringProjectPath: '/delete',
+ });
+ });
+
+ afterEach(() => {
+ if (wrapper.destroy) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('default state', () => {
+ it('to match the default snapshot', () => {
+ wrapper = shallowMount(SelfMonitor, { store });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders header text', () => {
+ wrapper = shallowMount(SelfMonitor, { store });
+
+ expect(wrapper.find('.js-section-header').text()).toBe('Self monitoring');
+ });
+
+ describe('expand/collapse button', () => {
+ it('renders as an expand button by default', () => {
+ wrapper = shallowMount(SelfMonitor, { store });
+
+ const button = wrapper.find(GlButton);
+
+ expect(button.text()).toBe('Expand');
+ });
+ });
+
+ describe('sub-header', () => {
+ it('renders descriptive text', () => {
+ wrapper = shallowMount(SelfMonitor, { store });
+
+ expect(wrapper.find('.js-section-sub-header').text()).toContain(
+ 'Enable or disable instance self monitoring',
+ );
+ });
+ });
+
+ describe('settings-content', () => {
+ it('renders the form description without a link', () => {
+ wrapper = shallowMount(SelfMonitor, { store });
+
+ expect(wrapper.vm.selfMonitoringFormText).toContain(
+ 'Enabling this feature creates a project that can be used to monitor the health of your instance.',
+ );
+ });
+
+ it('renders the form description with a link', () => {
+ store = createStore({
+ projectEnabled: true,
+ selfMonitorProjectCreated: true,
+ createSelfMonitoringProjectPath: '/create',
+ deleteSelfMonitoringProjectPath: '/delete',
+ });
+
+ wrapper = shallowMount(SelfMonitor, { store });
+
+ expect(wrapper.vm.selfMonitoringFormText).toContain('<a href="http://localhost/">');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
new file mode 100644
index 00000000000..344dbf11954
--- /dev/null
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -0,0 +1,255 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import statusCodes from '~/lib/utils/http_status';
+import * as actions from '~/self_monitor/store/actions';
+import * as types from '~/self_monitor/store/mutation_types';
+import createState from '~/self_monitor/store/state';
+
+describe('self monitor actions', () => {
+ let state;
+ let mock;
+
+ beforeEach(() => {
+ state = createState();
+ mock = new MockAdapter(axios);
+ });
+
+ describe('setSelfMonitor', () => {
+ it('commits the SET_ENABLED mutation', done => {
+ testAction(
+ actions.setSelfMonitor,
+ null,
+ state,
+ [{ type: types.SET_ENABLED, payload: null }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('resetAlert', () => {
+ it('commits the SET_ENABLED mutation', done => {
+ testAction(
+ actions.resetAlert,
+ null,
+ state,
+ [{ type: types.SET_SHOW_ALERT, payload: false }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestCreateProject', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ state.createProjectEndpoint = '/create';
+ state.createProjectStatusEndpoint = '/create_status';
+ mock.onPost(state.createProjectEndpoint).reply(statusCodes.ACCEPTED, {
+ job_id: '123',
+ });
+ mock.onGet(state.createProjectStatusEndpoint).reply(statusCodes.OK, {
+ project_full_path: '/self-monitor-url',
+ });
+ });
+
+ it('dispatches status request with job data', done => {
+ testAction(
+ actions.requestCreateProject,
+ null,
+ state,
+ [
+ {
+ type: types.SET_LOADING,
+ payload: true,
+ },
+ ],
+ [
+ {
+ type: 'requestCreateProjectStatus',
+ payload: '123',
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches success with project path', done => {
+ testAction(
+ actions.requestCreateProjectStatus,
+ null,
+ state,
+ [],
+ [
+ {
+ type: 'requestCreateProjectSuccess',
+ payload: { project_full_path: '/self-monitor-url' },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ state.createProjectEndpoint = '/create';
+ mock.onPost(state.createProjectEndpoint).reply(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ actions.requestCreateProject,
+ null,
+ state,
+ [
+ {
+ type: types.SET_LOADING,
+ payload: true,
+ },
+ ],
+ [
+ {
+ type: 'requestCreateProjectError',
+ payload: new Error('Request failed with status code 500'),
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('requestCreateProjectSuccess', () => {
+ it('should commit the received data', done => {
+ testAction(
+ actions.requestCreateProjectSuccess,
+ { project_full_path: '/self-monitor-url' },
+ state,
+ [
+ { type: types.SET_LOADING, payload: false },
+ { type: types.SET_PROJECT_URL, payload: '/self-monitor-url' },
+ {
+ type: types.SET_ALERT_CONTENT,
+ payload: {
+ actionName: 'viewSelfMonitorProject',
+ actionText: 'View project',
+ message: 'Self monitoring project has been successfully created.',
+ },
+ },
+ { type: types.SET_SHOW_ALERT, payload: true },
+ { type: types.SET_PROJECT_CREATED, payload: true },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('deleteSelfMonitorProject', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ state.deleteProjectEndpoint = '/delete';
+ state.deleteProjectStatusEndpoint = '/delete-status';
+ mock.onDelete(state.deleteProjectEndpoint).reply(statusCodes.ACCEPTED, {
+ job_id: '456',
+ });
+ mock.onGet(state.deleteProjectStatusEndpoint).reply(statusCodes.OK, {
+ status: 'success',
+ });
+ });
+
+ it('dispatches status request with job data', done => {
+ testAction(
+ actions.requestDeleteProject,
+ null,
+ state,
+ [
+ {
+ type: types.SET_LOADING,
+ payload: true,
+ },
+ ],
+ [
+ {
+ type: 'requestDeleteProjectStatus',
+ payload: '456',
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches success with status', done => {
+ testAction(
+ actions.requestDeleteProjectStatus,
+ null,
+ state,
+ [],
+ [
+ {
+ type: 'requestDeleteProjectSuccess',
+ payload: { status: 'success' },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ state.deleteProjectEndpoint = '/delete';
+ mock.onDelete(state.deleteProjectEndpoint).reply(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ actions.requestDeleteProject,
+ null,
+ state,
+ [
+ {
+ type: types.SET_LOADING,
+ payload: true,
+ },
+ ],
+ [
+ {
+ type: 'requestDeleteProjectError',
+ payload: new Error('Request failed with status code 500'),
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('requestDeleteProjectSuccess', () => {
+ it('should commit mutations to remove previously set data', done => {
+ testAction(
+ actions.requestDeleteProjectSuccess,
+ null,
+ state,
+ [
+ { type: types.SET_PROJECT_URL, payload: '' },
+ { type: types.SET_PROJECT_CREATED, payload: false },
+ {
+ type: types.SET_ALERT_CONTENT,
+ payload: {
+ actionName: 'createProject',
+ actionText: 'Undo',
+ message: 'Self monitoring project has been successfully deleted.',
+ },
+ },
+ { type: types.SET_SHOW_ALERT, payload: true },
+ { type: types.SET_LOADING, payload: false },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/self_monitor/store/mutations_spec.js b/spec/frontend/self_monitor/store/mutations_spec.js
new file mode 100644
index 00000000000..5282ae3b2f5
--- /dev/null
+++ b/spec/frontend/self_monitor/store/mutations_spec.js
@@ -0,0 +1,64 @@
+import mutations from '~/self_monitor/store/mutations';
+import createState from '~/self_monitor/store/state';
+
+describe('self monitoring mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = createState();
+ });
+
+ describe('SET_ENABLED', () => {
+ it('sets selfMonitor', () => {
+ mutations.SET_ENABLED(localState, true);
+
+ expect(localState.projectEnabled).toBe(true);
+ });
+ });
+
+ describe('SET_PROJECT_CREATED', () => {
+ it('sets projectCreated', () => {
+ mutations.SET_PROJECT_CREATED(localState, true);
+
+ expect(localState.projectCreated).toBe(true);
+ });
+ });
+
+ describe('SET_SHOW_ALERT', () => {
+ it('sets showAlert', () => {
+ mutations.SET_SHOW_ALERT(localState, true);
+
+ expect(localState.showAlert).toBe(true);
+ });
+ });
+
+ describe('SET_PROJECT_URL', () => {
+ it('sets projectPath', () => {
+ mutations.SET_PROJECT_URL(localState, '/url/');
+
+ expect(localState.projectPath).toBe('/url/');
+ });
+ });
+
+ describe('SET_LOADING', () => {
+ it('sets loading', () => {
+ mutations.SET_LOADING(localState, true);
+
+ expect(localState.loading).toBe(true);
+ });
+ });
+
+ describe('SET_ALERT_CONTENT', () => {
+ it('set alertContent', () => {
+ const alertContent = {
+ message: 'success',
+ actionText: 'undo',
+ actionName: 'createProject',
+ };
+
+ mutations.SET_ALERT_CONTENT(localState, alertContent);
+
+ expect(localState.alertContent).toBe(alertContent);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index 4b4a710df2d..2411fe8ad89 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -77,7 +77,7 @@ describe('diffs/components/app', () => {
beforeEach(done => {
const fetchResolver = () => {
store.state.diffs.retrievingBatches = false;
- return Promise.resolve();
+ return Promise.resolve({ real_size: 100 });
};
spyOn(window, 'requestIdleCallback').and.callFake(fn => fn());
createComponent();
@@ -229,6 +229,7 @@ describe('diffs/components/app', () => {
});
it('calls fetchDiffFiles if diffsBatchLoad is not enabled', done => {
+ expect(wrapper.vm.diffFilesLength).toEqual(0);
wrapper.vm.glFeatures.diffsBatchLoad = false;
wrapper.vm.fetchData(false);
@@ -238,12 +239,14 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
+ expect(wrapper.vm.diffFilesLength).toEqual(100);
done();
});
});
it('calls batch methods if diffsBatchLoad is enabled, and not latest version', done => {
+ expect(wrapper.vm.diffFilesLength).toEqual(0);
wrapper.vm.glFeatures.diffsBatchLoad = true;
wrapper.vm.isLatestVersion = () => false;
wrapper.vm.fetchData(false);
@@ -254,11 +257,13 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
+ expect(wrapper.vm.diffFilesLength).toEqual(100);
done();
});
});
it('calls batch methods if diffsBatchLoad is enabled, and latest version', done => {
+ expect(wrapper.vm.diffFilesLength).toEqual(0);
wrapper.vm.glFeatures.diffsBatchLoad = true;
wrapper.vm.fetchData(false);
@@ -268,6 +273,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
+ expect(wrapper.vm.diffFilesLength).toEqual(100);
done();
});
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 436d7338361..af2dd7b4f93 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -141,6 +141,13 @@ describe('DiffsStoreActions', () => {
done();
},
);
+
+ fetchDiffFiles({ state: { endpoint }, commit: () => null })
+ .then(data => {
+ expect(data).toEqual(res);
+ done();
+ })
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index eab5703dfb2..9e628fdd540 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -263,14 +263,6 @@ describe('Diffs Module Getters', () => {
});
});
- describe('diffFilesLength', () => {
- it('returns length of diff files', () => {
- localState.diffFiles.push('test', 'test 2');
-
- expect(getters.diffFilesLength(localState)).toBe(2);
- });
- });
-
describe('currentDiffIndex', () => {
it('returns index of currently selected diff in diffList', () => {
localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
index a1c00e99927..0ea767e087d 100644
--- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -52,19 +52,6 @@ describe('new file modal component', () => {
expect(templateFilesEl instanceof Element).toBeTruthy();
}
});
-
- describe('createEntryInStore', () => {
- it('$emits create', () => {
- spyOn(vm, 'createTempEntry');
-
- vm.submitForm();
-
- expect(vm.createTempEntry).toHaveBeenCalledWith({
- name: 'testing',
- type,
- });
- });
- });
});
});
@@ -145,31 +132,19 @@ describe('new file modal component', () => {
vm = createComponentWithStore(Component, store).$mount();
const flashSpy = spyOnDependency(modal, 'flash');
- vm.submitForm();
- expect(flashSpy).toHaveBeenCalled();
- });
+ expect(flashSpy).not.toHaveBeenCalled();
- it('calls createTempEntry when target path does not exist', () => {
- const store = createStore();
- store.state.entryModal = {
- type: 'rename',
- path: 'test-path/test',
- entry: {
- name: 'test',
- type: 'blob',
- path: 'test-path1/test',
- },
- };
-
- vm = createComponentWithStore(Component, store).$mount();
- spyOn(vm, 'createTempEntry').and.callFake(() => Promise.resolve());
vm.submitForm();
- expect(vm.createTempEntry).toHaveBeenCalledWith({
- name: 'test-path1',
- type: 'tree',
- });
+ expect(flashSpy).toHaveBeenCalledWith(
+ 'The name "test-path/test" is already taken in this directory.',
+ 'alert',
+ jasmine.anything(),
+ null,
+ false,
+ true,
+ );
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index 6a7116d87f2..bd51222ac3c 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -201,35 +201,30 @@ describe('IDE store project actions', () => {
});
describe('showEmptyState', () => {
- it('commits proper mutations when supplied error is 404', done => {
+ it('creates a blank tree and sets loading state to false', done => {
testAction(
showEmptyState,
- {
- err: {
- response: {
- status: 404,
- },
- },
- projectId: 'abc/def',
- branchId: 'master',
- },
+ { projectId: 'abc/def', branchId: 'master' },
store.state,
[
- {
- type: 'CREATE_TREE',
- payload: {
- treePath: 'abc/def/master',
- },
- },
+ { type: 'CREATE_TREE', payload: { treePath: 'abc/def/master' } },
{
type: 'TOGGLE_LOADING',
- payload: {
- entry: store.state.trees['abc/def/master'],
- forceValue: false,
- },
+ payload: { entry: store.state.trees['abc/def/master'], forceValue: false },
},
],
- [],
+ jasmine.any(Object),
+ done,
+ );
+ });
+
+ it('sets the currentBranchId to the branchId that was passed', done => {
+ testAction(
+ showEmptyState,
+ { projectId: 'abc/def', branchId: 'master' },
+ store.state,
+ jasmine.any(Object),
+ [{ type: 'setCurrentBranchId', payload: 'master' }],
done,
);
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 8abd9c38514..9c24f20ca9c 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -206,13 +206,17 @@ describe('Multi-file store actions', () => {
describe('blob', () => {
it('creates temp file', done => {
+ const name = 'test';
+
store
.dispatch('createTempEntry', {
- name: 'test',
+ name,
branchId: 'mybranch',
type: 'blob',
})
- .then(f => {
+ .then(() => {
+ const f = store.state.entries[name];
+
expect(f.tempFile).toBeTruthy();
expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
@@ -222,13 +226,17 @@ describe('Multi-file store actions', () => {
});
it('adds tmp file to open files', done => {
+ const name = 'test';
+
store
.dispatch('createTempEntry', {
- name: 'test',
+ name,
branchId: 'mybranch',
type: 'blob',
})
- .then(f => {
+ .then(() => {
+ const f = store.state.entries[name];
+
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(f.name);
@@ -238,13 +246,17 @@ describe('Multi-file store actions', () => {
});
it('adds tmp file to changed files', done => {
+ const name = 'test';
+
store
.dispatch('createTempEntry', {
- name: 'test',
+ name,
branchId: 'mybranch',
type: 'blob',
})
- .then(f => {
+ .then(() => {
+ const f = store.state.entries[name];
+
expect(store.state.changedFiles.length).toBe(1);
expect(store.state.changedFiles[0].name).toBe(f.name);
@@ -292,7 +304,9 @@ describe('Multi-file store actions', () => {
type: 'blob',
})
.then(() => {
- expect(document.querySelector('.flash-alert')).not.toBeNull();
+ expect(document.querySelector('.flash-alert')?.textContent.trim()).toEqual(
+ `The name "${f.name}" is already taken in this directory.`,
+ );
done();
})
@@ -604,36 +618,98 @@ describe('Multi-file store actions', () => {
);
});
- it('if renamed, reverts the rename before deleting', () => {
- const testEntry = {
- path: 'test',
- name: 'test',
- prevPath: 'lorem/ipsum',
- prevName: 'ipsum',
- prevParentPath: 'lorem',
- };
+ describe('when renamed', () => {
+ let testEntry;
- store.state.entries = { test: testEntry };
- testAction(
- deleteEntry,
- testEntry.path,
- store.state,
- [],
- [
- {
- type: 'renameEntry',
- payload: {
- path: testEntry.path,
- name: testEntry.prevName,
- parentPath: testEntry.prevParentPath,
- },
- },
- {
- type: 'deleteEntry',
- payload: testEntry.prevPath,
- },
- ],
- );
+ beforeEach(() => {
+ testEntry = {
+ path: 'test',
+ name: 'test',
+ prevPath: 'test_old',
+ prevName: 'test_old',
+ prevParentPath: '',
+ };
+
+ store.state.entries = { test: testEntry };
+ });
+
+ describe('and previous does not exist', () => {
+ it('reverts the rename before deleting', done => {
+ testAction(
+ deleteEntry,
+ testEntry.path,
+ store.state,
+ [],
+ [
+ {
+ type: 'renameEntry',
+ payload: {
+ path: testEntry.path,
+ name: testEntry.prevName,
+ parentPath: testEntry.prevParentPath,
+ },
+ },
+ {
+ type: 'deleteEntry',
+ payload: testEntry.prevPath,
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('and previous exists', () => {
+ beforeEach(() => {
+ const oldEntry = {
+ path: testEntry.prevPath,
+ name: testEntry.prevName,
+ };
+
+ store.state.entries[oldEntry.path] = oldEntry;
+ });
+
+ it('does not revert rename before deleting', done => {
+ testAction(
+ deleteEntry,
+ testEntry.path,
+ store.state,
+ [{ type: types.DELETE_ENTRY, payload: testEntry.path }],
+ [
+ { type: 'burstUnusedSeal' },
+ { type: 'stageChange', payload: testEntry.path },
+ { type: 'triggerFilesChange' },
+ ],
+ done,
+ );
+ });
+
+ it('when previous is deleted, it reverts rename before deleting', done => {
+ store.state.entries[testEntry.prevPath].deleted = true;
+
+ testAction(
+ deleteEntry,
+ testEntry.path,
+ store.state,
+ [],
+ [
+ {
+ type: 'renameEntry',
+ payload: {
+ path: testEntry.path,
+ name: testEntry.prevName,
+ parentPath: testEntry.prevParentPath,
+ },
+ },
+ {
+ type: 'deleteEntry',
+ payload: testEntry.prevPath,
+ },
+ ],
+ done,
+ );
+ });
+ });
});
it('bursts unused seal', done => {
@@ -918,6 +994,103 @@ describe('Multi-file store actions', () => {
.then(done)
.catch(done.fail);
});
+
+ describe('with file in directory', () => {
+ const parentPath = 'original-dir';
+ const newParentPath = 'new-dir';
+ const fileName = 'test.md';
+ const filePath = `${parentPath}/${fileName}`;
+
+ let rootDir;
+
+ beforeEach(() => {
+ const parentEntry = file(parentPath, parentPath, 'tree');
+ const fileEntry = file(filePath, filePath, 'blob', parentEntry);
+ rootDir = {
+ tree: [],
+ };
+
+ Object.assign(store.state, {
+ entries: {
+ [parentPath]: {
+ ...parentEntry,
+ tree: [fileEntry],
+ },
+ [filePath]: fileEntry,
+ },
+ trees: {
+ '/': rootDir,
+ },
+ });
+ });
+
+ it('creates new directory', done => {
+ expect(store.state.entries[newParentPath]).toBeUndefined();
+
+ store
+ .dispatch('renameEntry', { path: filePath, name: fileName, parentPath: newParentPath })
+ .then(() => {
+ expect(store.state.entries[newParentPath]).toEqual(
+ jasmine.objectContaining({
+ path: newParentPath,
+ type: 'tree',
+ tree: jasmine.arrayContaining([
+ store.state.entries[`${newParentPath}/${fileName}`],
+ ]),
+ }),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('when new directory exists', () => {
+ let newDir;
+
+ beforeEach(() => {
+ newDir = file(newParentPath, newParentPath, 'tree');
+
+ store.state.entries[newDir.path] = newDir;
+ rootDir.tree.push(newDir);
+ });
+
+ it('inserts in new directory', done => {
+ expect(newDir.tree).toEqual([]);
+
+ store
+ .dispatch('renameEntry', {
+ path: filePath,
+ name: fileName,
+ parentPath: newParentPath,
+ })
+ .then(() => {
+ expect(newDir.tree).toEqual([store.state.entries[`${newParentPath}/${fileName}`]]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('when new directory is deleted, it undeletes it', done => {
+ store.dispatch('deleteEntry', newParentPath);
+
+ expect(store.state.entries[newParentPath].deleted).toBe(true);
+ expect(rootDir.tree.some(x => x.path === newParentPath)).toBe(false);
+
+ store
+ .dispatch('renameEntry', {
+ path: filePath,
+ name: fileName,
+ parentPath: newParentPath,
+ })
+ .then(() => {
+ expect(store.state.entries[newParentPath].deleted).toBe(false);
+ expect(rootDir.tree.some(x => x.path === newParentPath)).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
});
});
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/repository_link_filter_spec.rb
index 9f467d7a6fd..c87f452a3df 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/repository_link_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe Banzai::Filter::RelativeLinkFilter do
+describe Banzai::Filter::RepositoryLinkFilter do
include GitHelpers
include RepoHelpers
@@ -128,11 +128,6 @@ describe Banzai::Filter::RelativeLinkFilter do
expect { filter(act) }.not_to raise_error
end
- it 'does not raise an exception on URIs containing invalid utf-8 byte sequences in uploads' do
- act = link("/uploads/%FF")
- expect { filter(act) }.not_to raise_error
- end
-
it 'does not raise an exception on URIs containing invalid utf-8 byte sequences in context requested path' do
expect { filter(link("files/test.md"), requested_path: '%FF') }.not_to raise_error
end
@@ -147,11 +142,6 @@ describe Banzai::Filter::RelativeLinkFilter do
expect { filter(act) }.not_to raise_error
end
- it 'does not raise an exception with a space in the path' do
- act = link("/uploads/d18213acd3732630991986120e167e3d/Landscape_8.jpg \nBut here's some more unexpected text :smile:)")
- expect { filter(act) }.not_to raise_error
- end
-
it 'ignores ref if commit is passed' do
doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') )
expect(doc.at_css('a')['href'])
@@ -350,166 +340,4 @@ describe Banzai::Filter::RelativeLinkFilter do
include_examples :valid_repository
end
-
- context 'with a /upload/ URL' do
- # not needed
- let(:commit) { nil }
- let(:ref) { nil }
- let(:requested_path) { nil }
- let(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
- let(:relative_path) { "/#{project.full_path}#{upload_path}" }
-
- context 'to a project upload' do
- shared_examples 'rewrite project uploads' do
- context 'with an absolute URL' do
- let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
- let(:only_path) { false }
-
- it 'rewrites the link correctly' do
- doc = filter(link(upload_path))
-
- expect(doc.at_css('a')['href']).to eq(absolute_path)
- end
- end
-
- it 'rebuilds relative URL for a link' do
- doc = filter(link(upload_path))
- expect(doc.at_css('a')['href']).to eq(relative_path)
-
- doc = filter(nested(link(upload_path)))
- expect(doc.at_css('a')['href']).to eq(relative_path)
- end
-
- it 'rebuilds relative URL for an image' do
- doc = filter(image(upload_path))
- expect(doc.at_css('img')['src']).to eq(relative_path)
-
- doc = filter(nested(image(upload_path)))
- expect(doc.at_css('img')['src']).to eq(relative_path)
- end
-
- it 'does not modify absolute URL' do
- doc = filter(link('http://example.com'))
- expect(doc.at_css('a')['href']).to eq 'http://example.com'
- end
-
- it 'supports unescaped Unicode filenames' do
- path = '/uploads/한글.png'
- doc = filter(link(path))
-
- expect(doc.at_css('a')['href']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
- end
-
- it 'supports escaped Unicode filenames' do
- path = '/uploads/한글.png'
- escaped = Addressable::URI.escape(path)
- doc = filter(image(escaped))
-
- expect(doc.at_css('img')['src']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
- end
- end
-
- context 'without project repository access' do
- let(:project) { create(:project, :repository, repository_access_level: ProjectFeature::PRIVATE) }
-
- it_behaves_like 'rewrite project uploads'
- end
-
- context 'with project repository access' do
- it_behaves_like 'rewrite project uploads'
- end
- end
-
- context 'to a group upload' do
- let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
- let(:group) { create(:group) }
- let(:project) { nil }
- let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" }
-
- context 'with an absolute URL' do
- let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
- let(:only_path) { false }
-
- it 'rewrites the link correctly' do
- doc = filter(upload_link)
-
- expect(doc.at_css('a')['href']).to eq(absolute_path)
- end
- end
-
- it 'rewrites the link correctly' do
- doc = filter(upload_link)
-
- expect(doc.at_css('a')['href']).to eq(relative_path)
- end
-
- it 'rewrites the link correctly for subgroup' do
- group.update!(parent: create(:group))
-
- doc = filter(upload_link)
-
- expect(doc.at_css('a')['href']).to eq(relative_path)
- end
-
- it 'does not modify absolute URL' do
- doc = filter(link('http://example.com'))
-
- expect(doc.at_css('a')['href']).to eq 'http://example.com'
- end
- end
-
- context 'to a personal snippet' do
- let(:group) { nil }
- let(:project) { nil }
- let(:relative_path) { '/uploads/-/system/personal_snippet/6/674e4f07fbf0a7736c3439212896e51a/example.tar.gz' }
-
- context 'with an absolute URL' do
- let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
- let(:only_path) { false }
-
- it 'rewrites the link correctly' do
- doc = filter(link(relative_path))
-
- expect(doc.at_css('a')['href']).to eq(absolute_path)
- end
- end
-
- context 'with a relative URL root' do
- let(:gitlab_root) { '/gitlab' }
- let(:absolute_path) { Gitlab.config.gitlab.url + gitlab_root + relative_path }
-
- before do
- stub_config_setting(relative_url_root: gitlab_root)
- end
-
- context 'with an absolute URL' do
- let(:only_path) { false }
-
- it 'rewrites the link correctly' do
- doc = filter(link(relative_path))
-
- expect(doc.at_css('a')['href']).to eq(absolute_path)
- end
- end
-
- it 'rewrites the link correctly' do
- doc = filter(link(relative_path))
-
- expect(doc.at_css('a')['href']).to eq(gitlab_root + relative_path)
- end
- end
-
- it 'rewrites the link correctly' do
- doc = filter(link(relative_path))
-
- expect(doc.at_css('a')['href']).to eq(relative_path)
- end
-
- it 'does not modify absolute URL' do
- doc = filter(link('http://example.com'))
-
- expect(doc.at_css('a')['href']).to eq 'http://example.com'
- end
- end
- end
end
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
new file mode 100644
index 00000000000..3f181dce7bc
--- /dev/null
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::UploadLinkFilter do
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!(
+ project: project,
+ group: group,
+ only_path: only_path
+ )
+
+ described_class.call(doc, contexts)
+ end
+
+ def image(path)
+ %(<img src="#{path}" />)
+ end
+
+ def video(path)
+ %(<video src="#{path}"></video>)
+ end
+
+ def audio(path)
+ %(<audio src="#{path}"></audio>)
+ end
+
+ def link(path)
+ %(<a href="#{path}">#{path}</a>)
+ end
+
+ def nested(element)
+ %(<div>#{element}</div>)
+ end
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let(:group) { nil }
+ let(:project_path) { project.full_path }
+ let(:only_path) { true }
+ let(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' }
+ let(:relative_path) { "/#{project.full_path}#{upload_path}" }
+
+ context 'to a project upload' do
+ context 'with an absolute URL' do
+ let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
+ let(:only_path) { false }
+
+ it 'rewrites the link correctly' do
+ doc = filter(link(upload_path))
+
+ expect(doc.at_css('a')['href']).to eq(absolute_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+ end
+
+ it 'rebuilds relative URL for a link' do
+ doc = filter(link(upload_path))
+
+ expect(doc.at_css('a')['href']).to eq(relative_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+
+ doc = filter(nested(link(upload_path)))
+
+ expect(doc.at_css('a')['href']).to eq(relative_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+
+ it 'rebuilds relative URL for an image' do
+ doc = filter(image(upload_path))
+
+ expect(doc.at_css('img')['src']).to eq(relative_path)
+ expect(doc.at_css('img').classes).to include('gfm')
+
+ doc = filter(nested(image(upload_path)))
+
+ expect(doc.at_css('img')['src']).to eq(relative_path)
+ expect(doc.at_css('img').classes).to include('gfm')
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ expect(doc.at_css('a').classes).not_to include('gfm')
+ end
+
+ it 'supports unescaped Unicode filenames' do
+ path = '/uploads/한글.png'
+ doc = filter(link(path))
+
+ expect(doc.at_css('a')['href']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+
+ it 'supports escaped Unicode filenames' do
+ path = '/uploads/한글.png'
+ escaped = Addressable::URI.escape(path)
+ doc = filter(image(escaped))
+
+ expect(doc.at_css('img')['src']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
+ expect(doc.at_css('img').classes).to include('gfm')
+ end
+ end
+
+ context 'to a group upload' do
+ let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
+ let_it_be(:group) { create(:group) }
+ let(:project) { nil }
+ let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" }
+
+ context 'with an absolute URL' do
+ let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
+ let(:only_path) { false }
+
+ it 'rewrites the link correctly' do
+ doc = filter(upload_link)
+
+ expect(doc.at_css('a')['href']).to eq(absolute_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+ end
+
+ it 'rewrites the link correctly' do
+ doc = filter(upload_link)
+
+ expect(doc.at_css('a')['href']).to eq(relative_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+
+ it 'rewrites the link correctly for subgroup' do
+ group.update!(parent: create(:group))
+
+ doc = filter(upload_link)
+
+ expect(doc.at_css('a')['href']).to eq(relative_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ expect(doc.at_css('a').classes).not_to include('gfm')
+ end
+ end
+
+ context 'to a personal snippet' do
+ let(:group) { nil }
+ let(:project) { nil }
+ let(:relative_path) { '/uploads/-/system/personal_snippet/6/674e4f07fbf0a7736c3439212896e51a/example.tar.gz' }
+
+ context 'with an absolute URL' do
+ let(:absolute_path) { Gitlab.config.gitlab.url + relative_path }
+ let(:only_path) { false }
+
+ it 'rewrites the link correctly' do
+ doc = filter(link(relative_path))
+
+ expect(doc.at_css('a')['href']).to eq(absolute_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+ end
+
+ context 'with a relative URL root' do
+ let(:gitlab_root) { '/gitlab' }
+ let(:absolute_path) { Gitlab.config.gitlab.url + gitlab_root + relative_path }
+
+ before do
+ stub_config_setting(relative_url_root: gitlab_root)
+ end
+
+ context 'with an absolute URL' do
+ let(:only_path) { false }
+
+ it 'rewrites the link correctly' do
+ doc = filter(link(relative_path))
+
+ expect(doc.at_css('a')['href']).to eq(absolute_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+ end
+
+ it 'rewrites the link correctly' do
+ doc = filter(link(relative_path))
+
+ expect(doc.at_css('a')['href']).to eq(gitlab_root + relative_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+ end
+
+ it 'rewrites the link correctly' do
+ doc = filter(link(relative_path))
+
+ expect(doc.at_css('a')['href']).to eq(relative_path)
+ expect(doc.at_css('a').classes).to include('gfm')
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ expect(doc.at_css('a').classes).not_to include('gfm')
+ end
+ end
+
+ context 'invalid input' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:name, :href) do
+ 'invalid URI' | '://foo'
+ 'invalid UTF-8 byte sequences' | '%FF'
+ 'garbled path' | 'open(/var/tmp/):%20/location%0Afrom:%20/test'
+ 'whitespace' | "d18213acd3732630991986120e167e3d/Landscape_8.jpg\nand more"
+ end
+
+ with_them do
+ it { expect { filter(link("/uploads/#{href}")) }.not_to raise_error }
+ end
+ end
+end
diff --git a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
new file mode 100644
index 00000000000..ab72354edcf
--- /dev/null
+++ b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Pipeline::PostProcessPipeline do
+ context 'when a document only has upload links' do
+ it 'does not make any Gitaly calls', :request_store do
+ markdown = <<-MARKDOWN.strip_heredoc
+ [Relative Upload Link](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
+
+ ![Relative Upload Image](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
+ MARKDOWN
+
+ context = {
+ project: create(:project, :public, :repository),
+ ref: 'master'
+ }
+
+ Gitlab::GitalyClient.reset_counts
+
+ described_class.call(markdown, context)
+
+ expect(Gitlab::GitalyClient.get_request_count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb b/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb
index 3101e782b8f..3ccb2379936 100644
--- a/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys_spec.rb
@@ -37,6 +37,25 @@ describe Gitlab::BackgroundMigration::MigrateFingerprintSha256WithinKeys, :migra
expect(key_2.fingerprint_sha256).to eq('zMNbLekgdjtcgDv8VSC0z5lpdACMG3Q4PUoIz5+H2jM')
end
+ context 'with invalid keys' do
+ before do
+ key = Key.find(1017)
+ # double space after "ssh-rsa" leads to a
+ # OpenSSL::PKey::PKeyError in Net::SSH::KeyFactory.load_data_public_key
+ key.update_column(:key, key.key.gsub('ssh-rsa ', 'ssh-rsa '))
+ end
+
+ it 'ignores errors and does not set the fingerprint' do
+ fingerprint_migrator.perform(1, 10000)
+
+ key_1 = Key.find(1017)
+ key_2 = Key.find(1027)
+
+ expect(key_1.fingerprint_sha256).to be_nil
+ expect(key_2.fingerprint_sha256).not_to be_nil
+ end
+ end
+
it 'migrates all keys' do
expect(Key.where(fingerprint_sha256: nil).count).to eq(Key.all.count)
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 35b2993443f..103019d8dd8 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -10,8 +10,21 @@ module MarkdownMatchers
extend RSpec::Matchers::DSL
include Capybara::Node::Matchers
- # RelativeLinkFilter
- matcher :parse_relative_links do
+ # UploadLinkFilter
+ matcher :parse_upload_links do
+ set_default_markdown_messages
+
+ match do |actual|
+ link = actual.at_css('a:contains("Relative Upload Link")')
+ image = actual.at_css('img[alt="Relative Upload Image"]')
+
+ expect(link['href']).to eq("/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg")
+ expect(image['data-src']).to eq("/#{project.full_path}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg")
+ end
+ end
+
+ # RepositoryLinkFilter
+ matcher :parse_repository_links do
set_default_markdown_messages
match do |actual|