diff options
author | Mike Lewis <mlewis@gitlab.com> | 2019-02-18 21:38:24 +0000 |
---|---|---|
committer | Mike Lewis <mlewis@gitlab.com> | 2019-02-18 21:38:24 +0000 |
commit | 07c32a0df5306082fe800de1ae6e2c51325f46ef (patch) | |
tree | 2d810bcd76a993ac71224de5b5f0b6a7707127df | |
parent | f1645c744fcf8677ae14714914c4c4ffb9d5b17e (diff) | |
parent | fe10964a6884162b9272ec3a32a5736c2a997ab2 (diff) | |
download | gitlab-ce-07c32a0df5306082fe800de1ae6e2c51325f46ef.tar.gz |
Merge branch 'master' into 'template-improvements-for-documentation'
# Conflicts:
# .gitlab/merge_request_templates/Documentation.md
568 files changed, 8298 insertions, 2269 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1ea9ce1f497..c5cd006a289 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -509,6 +509,7 @@ rspec-mysql: parallel: 50 .rspec-quarantine: &rspec-quarantine + retry: 0 script: - export CACHE_CLASSES=true - scripts/gitaly-test-spawn diff --git a/.gitlab/merge_request_templates/Change documentation location.md b/.gitlab/merge_request_templates/Change documentation location.md index b4a6d2bd3b4..c80af95d5e5 100644 --- a/.gitlab/merge_request_templates/Change documentation location.md +++ b/.gitlab/merge_request_templates/Change documentation location.md @@ -26,7 +26,7 @@ https://docs.gitlab.com/ce/development/documentation/index.html#changing-documen to the new document if there are any Disqus comments on the old document thread. - [ ] Update the link in `features.yml` (if applicable) - [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE - with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee). + with the changes as well (https://docs.gitlab.com/ce/development/documentation/index.html#cherry-picking-from-ce-to-ee). - [ ] Ping one of the technical writers for review. /label ~Documentation diff --git a/.gitlab/merge_request_templates/Database changes.md b/.gitlab/merge_request_templates/Database changes.md index 354393b60e0..3f457174492 100644 --- a/.gitlab/merge_request_templates/Database changes.md +++ b/.gitlab/merge_request_templates/Database changes.md @@ -16,7 +16,7 @@ Add a description of your merge request here. ## Database checklist -- [ ] Conforms to the [database guides](https://docs.gitlab.com/ee/development/README.html#databases-guides) +- [ ] Conforms to the [database guides](https://docs.gitlab.com/ee/development/README.html#database-guides) When adding migrations: @@ -49,10 +49,10 @@ When removing columns, tables, indexes or other structures: ## General checklist - [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary -- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/index.html#contributing-to-docs) +- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/) - [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html) - [ ] Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html) - [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) -- [ ] Conforms to the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) +- [ ] Conforms to the [style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html) /label ~database diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 00000000000..69de9a5dd13 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,30 @@ +{ + "extends": "stylelint-config-recommended", + "plugins": [ + "stylelint-scss" + ], + "rules": { + "no-descending-specificity": null, + "font-family-no-missing-generic-family-keyword": null, + "at-rule-no-unknown": [ true, { + ignoreAtRules: ["include", "each", "mixin", "extend", "if", "function", "for", "else", "return"] + }], + "selector-type-no-unknown": [true, { + "ignoreTypes": ["gl-emoji"] + }], + "unit-no-unknown" : [true, { + "ignoreFunctions": ["-webkit-image-set"] + }], + "scss/at-extend-no-missing-placeholder": null, + "scss/at-function-pattern": "^[a-z]+([a-z0-9-]+[a-z0-9]+)?$", + "scss/at-import-no-partial-leading-underscore": true, + "scss/at-import-partial-extension-blacklist": ["scss"], + "scss/at-mixin-pattern": "^[a-z]+([a-z0-9-]+[a-z0-9]+)?$", + "scss/at-rule-no-unknown": true, + "scss/dollar-variable-colon-space-after": "always", + "scss/dollar-variable-colon-space-before": "never", + "scss/dollar-variable-pattern": "^[_]?[a-z]+([a-z0-9-]+[a-z0-9]+)?$", + "scss/percent-placeholder-pattern": "^[a-z]+([a-z0-9-]+[a-z0-9]+)?$", + "scss/selector-no-redundant-nesting-selector": true, + } +} diff --git a/Dangerfile b/Dangerfile index 6a2c5cf2773..715a2bcbbae 100644 --- a/Dangerfile +++ b/Dangerfile @@ -11,3 +11,4 @@ danger.import_dangerfile(path: 'danger/commit_messages') danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies') danger.import_dangerfile(path: 'danger/prettier') danger.import_dangerfile(path: 'danger/eslint') +danger.import_dangerfile(path: 'danger/roulette') diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 815d5ca06d5..39893559155 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.19.0 +1.20.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 2bf50aaf17a..56b6be4ebb2 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.3.0 +8.3.1 @@ -143,7 +143,7 @@ gem 'diffy', '~> 3.1.0' gem 'rack', '2.0.6' group :unicorn do - gem 'unicorn', '~> 5.1.0' + gem 'unicorn', '~> 5.4.1' gem 'unicorn-worker-killer', '~> 0.4.4' end @@ -410,7 +410,7 @@ gem 'sys-filesystem', '~> 1.1.6' # SSH host key support gem 'net-ssh', '~> 5.0' -gem 'sshkey', '~> 1.9.0' +gem 'sshkey', '~> 2.0' # Required for ED25519 SSH host key support group :ed25519 do diff --git a/Gemfile.lock b/Gemfile.lock index 9aea8919287..7caeba15809 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -422,7 +422,7 @@ GEM activerecord kaminari-core (= 1.0.1) kaminari-core (1.0.1) - kgio (2.10.0) + kgio (2.11.2) knapsack (1.17.0) rake kubeclient (4.2.2) @@ -666,7 +666,7 @@ GEM rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (3.0.0) - raindrops (0.18.0) + raindrops (0.19.0) rake (12.3.2) rb-fsevent (0.10.2) rb-inotify (0.9.10) @@ -855,7 +855,7 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) sqlite3 (1.3.13) - sshkey (1.9.0) + sshkey (2.0.0) stackprof (0.2.10) state_machines (0.5.0) state_machines-activemodel (0.5.1) @@ -898,7 +898,7 @@ GEM unf_ext unf_ext (0.0.7.5) unicode-display_width (1.3.2) - unicorn (5.1.0) + unicorn (5.4.1) kgio (~> 2.6) raindrops (~> 0.7) unicorn-worker-killer (0.4.4) @@ -1157,7 +1157,7 @@ DEPENDENCIES spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) sprockets (~> 3.7.0) - sshkey (~> 1.9.0) + sshkey (~> 2.0) stackprof (~> 0.2.10) state_machines-activerecord (~> 0.5.1) sys-filesystem (~> 1.1.6) @@ -1169,7 +1169,7 @@ DEPENDENCIES u2f (~> 0.2.1) uglifier (~> 2.7.2) unf (~> 0.1.4) - unicorn (~> 5.1.0) + unicorn (~> 5.4.1) unicorn-worker-killer (~> 0.4.4) validates_hostname (~> 1.0.6) version_sorter (~> 2.1.0) diff --git a/PROCESS.md b/PROCESS.md index ad74ead8aab..1f99cebe081 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -108,7 +108,19 @@ Merge requests that make changes hidden behind a feature flag, or remove an existing feature flag because a feature is deemed stable, may be merged (and picked into the stable branches) up to the 19th of the month. Such merge requests should have the ~"feature flag" label assigned, and don't require a -corresponding exception request to be created. +corresponding exception request to be created. + +A level of common sense should be applied when deciding whether to have a feature +behind a feature flag off or on by default. + +The following guideliness can be applied to help make this decision: + +* If the feature is not fully ready or functioning, the feature flag should be disabled by default. +* If the feature is ready but there are concerns about performance or impact, the feature flag should be enabled by default, but +disabled via chatops before deployment on GitLab.com environments. If the performance concern is confirmed, the final release should have the feature flag disabled by default. +* In most other cases, the feature flag can be enabled by default. + +For more information on rolling out changes using feature flags, read [through the documentation](https://docs.gitlab.com/ee/development/rolling_out_changes_using_feature_flags.html). In order to build the final package and present the feature for self-hosted customers, the feature flag should be removed. This should happen before the diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 52d9f2f0322..9482a9f166d 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -36,13 +36,20 @@ export class CopyAsGFM { div.appendChild(el.cloneNode(true)); const html = div.innerHTML; + clipboardData.setData('text/plain', el.textContent); + clipboardData.setData('text/html', html); + // We are also setting this as fallback to transform the selection to gfm on paste + clipboardData.setData('text/x-gfm-html', html); + CopyAsGFM.nodeToGFM(el) .then(res => { - clipboardData.setData('text/plain', el.textContent); clipboardData.setData('text/x-gfm', res); - clipboardData.setData('text/html', html); }) - .catch(() => {}); + .catch(() => { + // Not showing the error as Firefox might doesn't allow + // it or other browsers who have a time limit on the execution + // of the copy event + }); } static pasteGFM(e) { @@ -51,11 +58,28 @@ export class CopyAsGFM { const text = clipboardData.getData('text/plain'); const gfm = clipboardData.getData('text/x-gfm'); - if (!gfm) return; + const gfmHtml = clipboardData.getData('text/x-gfm-html'); + if (!gfm && !gfmHtml) return; e.preventDefault(); - window.gl.utils.insertText(e.target, textBefore => { + // We have the original selection already converted to gfm + if (gfm) { + CopyAsGFM.insertPastedText(e.target, text, gfm); + } else { + // Due to the async copy call we are not able to produce gfm so we transform the cached HTML + const div = document.createElement('div'); + div.innerHTML = gfmHtml; + CopyAsGFM.nodeToGFM(div) + .then(transformedGfm => { + CopyAsGFM.insertPastedText(e.target, text, transformedGfm); + }) + .catch(() => {}); + } + } + + static insertPastedText(target, text, gfm) { + window.gl.utils.insertText(target, textBefore => { // If the text before the cursor contains an odd number of backticks, // we are either inside an inline code span that starts with 1 backtick // or a code block that starts with 3 backticks. diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index a689dfc3768..f3f341ece5c 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -221,7 +221,7 @@ export default { </script> <template> - <div class="board-list-component d-flex flex-column"> + <div class="board-list-component"> <div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues"> <gl-loading-icon /> </div> diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index f0ce2579ee7..8f47931d14a 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import { __ } from '~/locale'; import createFlash from '~/flash'; import { GlLoadingIcon } from '@gitlab/ui'; +import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; import eventHub from '../../notes/event_hub'; import CompareVersions from './compare_versions.vue'; import DiffFile from './diff_file.vue'; @@ -11,6 +12,13 @@ import NoChanges from './no_changes.vue'; import HiddenFilesWarning from './hidden_files_warning.vue'; import CommitWidget from './commit_widget.vue'; import TreeList from './tree_list.vue'; +import { + TREE_LIST_WIDTH_STORAGE_KEY, + INITIAL_TREE_WIDTH, + MIN_TREE_WIDTH, + MAX_TREE_WIDTH, + TREE_HIDE_STATS_WIDTH, +} from '../constants'; export default { name: 'DiffsApp', @@ -23,6 +31,7 @@ export default { CommitWidget, TreeList, GlLoadingIcon, + PanelResizer, }, props: { endpoint: { @@ -54,8 +63,12 @@ export default { }, }, data() { + const treeWidth = + parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH; + return { assignedDiscussions: false, + treeWidth, }; }, computed: { @@ -96,6 +109,9 @@ export default { this.startVersion.version_index === this.mergeRequestDiff.version_index) ); }, + hideFileStats() { + return this.treeWidth <= TREE_HIDE_STATS_WIDTH; + }, }, watch: { diffViewType() { @@ -142,6 +158,7 @@ export default { 'startRenderDiffsQueue', 'assignDiscussionsToDiff', 'setHighlightedRow', + 'cacheTreeListWidth', ]), fetchData() { this.fetchDiffFiles() @@ -184,6 +201,8 @@ export default { } }, }, + minTreeWidth: MIN_TREE_WIDTH, + maxTreeWidth: MAX_TREE_WIDTH, }; </script> @@ -209,7 +228,21 @@ export default { :data-can-create-note="getNoteableData.current_user.can_create_note" class="files d-flex prepend-top-default" > - <div v-show="showTreeList" class="diff-tree-list"><tree-list /></div> + <div + v-show="showTreeList" + :style="{ width: `${treeWidth}px` }" + class="diff-tree-list js-diff-tree-list" + > + <panel-resizer + :size.sync="treeWidth" + :start-size="treeWidth" + :min-size="$options.minTreeWidth" + :max-size="$options.maxTreeWidth" + side="right" + @resize-end="cacheTreeListWidth" + /> + <tree-list :hide-file-stats="hideFileStats" /> + </div> <div class="diff-files-holder"> <commit-widget v-if="commit" :commit="commit" /> <template v-if="renderDiffFiles"> diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 6dc2f5d3f68..cb92093db32 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -1,7 +1,8 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; -import EmptyFileViewer from '~/vue_shared/components/diff_viewer/viewers/empty_file.vue'; +import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue'; +import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue'; import InlineDiffView from './inline_diff_view.vue'; import ParallelDiffView from './parallel_diff_view.vue'; import NoteForm from '../../notes/components/note_form.vue'; @@ -9,6 +10,7 @@ import ImageDiffOverlay from './image_diff_overlay.vue'; import DiffDiscussions from './diff_discussions.vue'; import { IMAGE_DIFF_POSITION_TYPE } from '../constants'; import { getDiffMode } from '../store/utils'; +import { diffViewerModes } from '~/ide/constants'; export default { components: { @@ -18,7 +20,8 @@ export default { NoteForm, DiffDiscussions, ImageDiffOverlay, - EmptyFileViewer, + NotDiffableViewer, + NoPreviewViewer, }, props: { diffFile: { @@ -42,11 +45,17 @@ export default { diffMode() { return getDiffMode(this.diffFile); }, + diffViewerMode() { + return this.diffFile.viewer.name; + }, isTextFile() { - return this.diffFile.viewer.name === 'text'; + return this.diffViewerMode === diffViewerModes.text; + }, + noPreview() { + return this.diffViewerMode === diffViewerModes.no_preview; }, - errorMessage() { - return this.diffFile.viewer.error; + notDiffable() { + return this.diffViewerMode === diffViewerModes.not_diffable; }, diffFileCommentForm() { return this.getCommentFormForDiffFile(this.diffFile.file_hash); @@ -78,11 +87,10 @@ export default { <template> <div class="diff-content"> - <div v-if="!errorMessage" class="diff-viewer"> + <div class="diff-viewer"> <template v-if="isTextFile"> - <empty-file-viewer v-if="diffFile.empty" /> <inline-diff-view - v-else-if="isInlineView" + v-if="isInlineView" :diff-file="diffFile" :diff-lines="diffFile.highlighted_diff_lines || []" :help-page-path="helpPagePath" @@ -94,9 +102,12 @@ export default { :help-page-path="helpPagePath" /> </template> + <not-diffable-viewer v-else-if="notDiffable" /> + <no-preview-viewer v-else-if="noPreview" /> <diff-viewer v-else :diff-mode="diffMode" + :diff-viewer-mode="diffViewerMode" :new-path="diffFile.new_path" :new-sha="diffFile.diff_refs.head_sha" :old-path="diffFile.old_path" @@ -132,8 +143,5 @@ export default { </div> </diff-viewer> </div> - <div v-else class="diff-viewer"> - <div class="nothing-here-block" v-html="errorMessage"></div> - </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 449f7007077..1141a197c6a 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -7,6 +7,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import eventHub from '../../notes/event_hub'; import DiffFileHeader from './diff_file_header.vue'; import DiffContent from './diff_content.vue'; +import { diffViewerErrors } from '~/ide/constants'; export default { components: { @@ -33,15 +34,13 @@ export default { return { isLoadingCollapsedDiff: false, forkMessageVisible: false, + isCollapsed: this.file.viewer.collapsed || false, }; }, computed: { ...mapState('diffs', ['currentDiffFileId']), ...mapGetters(['isNotesFetched']), ...mapGetters('diffs', ['getDiffFileDiscussions']), - isCollapsed() { - return this.file.collapsed || false; - }, viewBlobLink() { return sprintf( __('You can %{linkStart}view the blob%{linkEnd} instead.'), @@ -52,17 +51,6 @@ export default { false, ); }, - showExpandMessage() { - return ( - this.isCollapsed || - (!this.file.highlighted_diff_lines && - !this.isLoadingCollapsedDiff && - !this.file.too_large && - this.file.text && - !this.file.renamed_file && - !this.file.mode_changed) - ); - }, showLoadingIcon() { return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); }, @@ -73,9 +61,15 @@ export default { this.file.parallel_diff_lines.length > 0 ); }, + isFileTooLarge() { + return this.file.viewer.error === diffViewerErrors.too_large; + }, + errorMessage() { + return this.file.viewer.error_message; + }, }, watch: { - 'file.collapsed': function fileCollapsedWatch(newVal, oldVal) { + isCollapsed: function fileCollapsedWatch(newVal, oldVal) { if (!newVal && oldVal && !this.hasDiffLines) { this.handleLoadCollapsedDiff(); } @@ -85,13 +79,13 @@ export default { eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff); }, methods: { - ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']), + ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff', 'setRenderIt']), handleToggle() { if (!this.hasDiffLines) { this.handleLoadCollapsedDiff(); } else { - this.file.collapsed = !this.file.collapsed; - this.file.renderIt = true; + this.isCollapsed = !this.isCollapsed; + this.setRenderIt(this.file); } }, handleLoadCollapsedDiff() { @@ -100,8 +94,8 @@ export default { this.loadCollapsedDiff(this.file) .then(() => { this.isLoadingCollapsedDiff = false; - this.file.collapsed = false; - this.file.renderIt = true; + this.isCollapsed = false; + this.setRenderIt(this.file); }) .then(() => { requestIdleCallback( @@ -164,21 +158,25 @@ export default { Cancel </button> </div> - - <diff-content - v-if="!isCollapsed && file.renderIt" - :class="{ hidden: isCollapsed || file.too_large }" - :diff-file="file" - :help-page-path="helpPagePath" - /> <gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" /> - <div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed"> - {{ __('This diff is collapsed.') }} - <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{ - __('Click to expand it.') - }}</a> - </div> - <div v-if="file.too_large" class="nothing-here-block diff-collapsed js-too-large-diff"> + <template v-else> + <div v-if="errorMessage" class="diff-viewer"> + <div class="nothing-here-block" v-html="errorMessage"></div> + </div> + <div v-else-if="isCollapsed" class="nothing-here-block diff-collapsed"> + {{ __('This diff is collapsed.') }} + <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{ + __('Click to expand it.') + }}</a> + </div> + <diff-content + v-else + :class="{ hidden: isCollapsed || isFileTooLarge }" + :diff-file="file" + :help-page-path="helpPagePath" + /> + </template> + <div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff"> {{ __('This source diff could not be displayed because it is too large.') }} <span v-html="viewBlobLink"></span> </div> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 60586d4a607..2b801898345 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -8,6 +8,7 @@ import FileIcon from '~/vue_shared/components/file_icon.vue'; import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; +import { diffViewerModes } from '~/ide/constants'; import EditButton from './edit_button.vue'; import DiffStats from './diff_stats.vue'; @@ -118,6 +119,12 @@ export default { gfmCopyText() { return `\`${this.diffFile.file_path}\``; }, + isFileRenamed() { + return this.diffFile.viewer.name === diffViewerModes.renamed; + }, + isModeChanged() { + return this.diffFile.viewer.name === diffViewerModes.mode_changed; + }, }, mounted() { polyfillSticky(this.$refs.header); @@ -165,7 +172,7 @@ export default { aria-hidden="true" css-classes="js-file-icon append-right-5" /> - <span v-if="diffFile.renamed_file"> + <span v-if="isFileRenamed"> <strong v-gl-tooltip :title="diffFile.old_path" @@ -193,7 +200,7 @@ export default { css-class="btn-default btn-transparent btn-clipboard" /> - <small v-if="diffFile.mode_changed" ref="fileMode"> + <small v-if="isModeChanged" ref="fileMode"> {{ diffFile.a_mode }} → {{ diffFile.b_mode }} </small> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 7e00b994541..8fc3af15bea 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -13,6 +13,12 @@ export default { Icon, FileRow, }, + props: { + hideFileStats: { + type: Boolean, + required: true, + }, + }, data() { return { search: '', @@ -40,6 +46,9 @@ export default { return acc; }, []); }, + fileRowExtraComponent() { + return this.hideFileStats ? null : FileRowStats; + }, }, methods: { ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), @@ -48,7 +57,6 @@ export default { }, }, shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`, - FileRowStats, diffTreeFiltering: gon.features && gon.features.diffTreeFiltering, }; </script> @@ -98,7 +106,7 @@ export default { :file="file" :level="0" :hide-extra-on-tree="true" - :extra-component="$options.FileRowStats" + :extra-component="fileRowExtraComponent" :show-changed-icon="true" @toggleTreeOpen="toggleTreeOpen" @clickFile="scrollToFile" diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index bd188d9de9e..7002655ea49 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -36,3 +36,9 @@ export const MR_TREE_SHOW_KEY = 'mr_tree_show'; export const TREE_TYPE = 'tree'; export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list'; export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace'; +export const TREE_LIST_WIDTH_STORAGE_KEY = 'mr_tree_list_width'; + +export const INITIAL_TREE_WIDTH = 320; +export const MIN_TREE_WIDTH = 240; +export const MAX_TREE_WIDTH = 400; +export const TREE_HIDE_STATS_WIDTH = 260; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 7fb66ce433b..82ff2e3be76 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -16,7 +16,9 @@ import { MR_TREE_SHOW_KEY, TREE_LIST_STORAGE_KEY, WHITESPACE_STORAGE_KEY, + TREE_LIST_WIDTH_STORAGE_KEY, } from '../constants'; +import { diffViewerModes } from '~/ide/constants'; export const setBaseConfig = ({ commit }, options) => { const { endpoint, projectPath } = options; @@ -91,7 +93,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi commit(types.RENDER_FILE, file); } - if (file.collapsed) { + if (file.viewer.collapsed) { eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`); scrollToElement(document.getElementById(file.file_hash)); } else { @@ -105,7 +107,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => { const checkItem = () => new Promise(resolve => { const nextFile = state.diffFiles.find( - file => !file.renderIt && (!file.collapsed || !file.text), + file => + !file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text), ); if (nextFile) { @@ -128,6 +131,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => { return checkItem(); }; +export const setRenderIt = ({ commit }, file) => commit(types.RENDER_FILE, file); + export const setInlineDiffViewType = ({ commit }) => { commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE); @@ -300,5 +305,9 @@ export const toggleFileFinder = ({ commit }, visible) => { commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible); }; +export const cacheTreeListWidth = (_, size) => { + localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 0e1ad654a2b..4e7e5306995 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -4,7 +4,8 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE; -export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed); +export const hasCollapsedFile = state => + state.diffFiles.some(file => file.viewer && file.viewer.collapsed); export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null); diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 7bbafe66199..5a27388863c 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -144,6 +144,7 @@ export default { if (left || right) { return { + ...line, left: line.left ? mapDiscussions(line.left) : null, right: line.right ? mapDiscussions(line.right, () => !left) : null, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index effb6202327..247d1e65fea 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -1,6 +1,6 @@ import _ from 'underscore'; -import { diffModes } from '~/ide/constants'; import { truncatePathMiddleToLength } from '~/lib/utils/text_utility'; +import { diffModes, diffViewerModes } from '~/ide/constants'; import { LINE_POSITION_LEFT, LINE_POSITION_RIGHT, @@ -161,6 +161,7 @@ export function addContextLines(options) { const normalizedParallelLines = contextLines.map(line => ({ left: line, right: line, + line_code: line.line_code, })); if (options.bottom) { @@ -247,7 +248,8 @@ export function prepareDiffData(diffData) { Object.assign(file, { renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, - collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, + collapsed: + file.viewer.name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED, discussions: [], }); } @@ -403,7 +405,9 @@ export const getDiffMode = diffFile => { const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]); return ( diffModes[diffModeKey] || - (diffFile.mode_changed && diffModes.mode_changed) || + (diffFile.viewer && + diffFile.viewer.name === diffViewerModes.mode_changed && + diffViewerModes.mode_changed) || diffModes.replaced ); }; diff --git a/app/assets/javascripts/emoji/no_emoji_validator.js b/app/assets/javascripts/emoji/no_emoji_validator.js new file mode 100644 index 00000000000..0fd4dd74953 --- /dev/null +++ b/app/assets/javascripts/emoji/no_emoji_validator.js @@ -0,0 +1,63 @@ +import { __ } from '~/locale'; +import emojiRegex from 'emoji-regex'; + +const invalidInputClass = 'gl-field-error-outline'; + +export default class NoEmojiValidator { + constructor(opts = {}) { + const container = opts.container || ''; + this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`); + + this.noEmojiEmelents.forEach(element => + element.addEventListener('input', this.eventHandler.bind(this)), + ); + } + + eventHandler(event) { + this.inputDomElement = event.target; + this.inputErrorMessage = this.inputDomElement.nextSibling; + + const { value } = this.inputDomElement; + + this.validatePattern(value); + this.setValidationStateAndMessage(); + } + + validatePattern(value) { + const pattern = emojiRegex(); + this.hasEmojis = new RegExp(pattern).test(value); + + if (this.hasEmojis) { + this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis')); + } else { + this.inputDomElement.setCustomValidity(''); + } + } + + setValidationStateAndMessage() { + if (!this.inputDomElement.checkValidity()) { + this.setInvalidState(); + } else { + this.clearFieldValidationState(); + } + } + + clearFieldValidationState() { + this.inputDomElement.classList.remove(invalidInputClass); + this.inputErrorMessage.classList.add('hide'); + } + + setInvalidState() { + this.inputDomElement.classList.add(invalidInputClass); + this.setErrorMessage(); + } + + setErrorMessage() { + if (this.hasEmojis) { + this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage; + } else { + this.inputErrorMessage.innerHTML = this.inputDomElement.title; + } + this.inputErrorMessage.classList.remove('hide'); + } +} diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index fba31f16d65..5090b0bdc3c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -163,7 +163,7 @@ export default class FilteredSearchVisualTokens { const tokenValueElement = tokenValueContainer.querySelector('.value'); tokenValueElement.innerText = tokenValue; - if (tokenValue === 'none' || tokenValue === 'any') { + if (['none', 'any'].includes(tokenValue.toLowerCase())) { return; } diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index 5119dbf32eb..11d5d9639b6 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -44,7 +44,7 @@ export default { <div class="d-flex ide-commit-editor-header align-items-center"> <file-icon :file-name="activeFile.name" :size="16" class="mr-2" /> <strong class="mr-2"> {{ activeFile.path }} </strong> - <changed-file-icon :file="activeFile" class="ml-0" /> + <changed-file-icon :file="activeFile" :is-centered="false" /> <div class="ml-auto"> <button v-if="!isStaged" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 04ecd4ba4e7..c9c4e9e86f8 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -51,8 +51,11 @@ export default { return __('Create file'); }, - isCreatingNew() { - return this.entryModal.type !== modalTypes.rename; + isCreatingNewFile() { + return this.entryModal.type === 'blob'; + }, + placeholder() { + return this.isCreatingNewFile ? 'dir/file_name' : 'dir/'; }, }, methods: { @@ -107,9 +110,12 @@ export default { v-model="entryName" type="text" class="form-control qa-full-file-path" - placeholder="/dir/file_name" + :placeholder="placeholder" /> - <ul v-if="isCreatingNew" class="prepend-top-default list-inline qa-template-list"> + <ul + v-if="isCreatingNewFile" + class="file-templates prepend-top-default list-inline qa-template-list" + > <li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item"> <button type="button" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 804ebae4555..7c560c89695 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -24,6 +24,22 @@ export const diffModes = { mode_changed: 'mode_changed', }; +export const diffViewerModes = Object.freeze({ + not_diffable: 'not_diffable', + no_preview: 'no_preview', + added: 'added', + deleted: 'deleted', + renamed: 'renamed', + mode_changed: 'mode_changed', + text: 'text', + image: 'image', +}); + +export const diffViewerErrors = Object.freeze({ + too_large: 'too_large', + stored_externally: 'server_side_but_stored_externally', +}); + export const rightSidebarViews = { pipelines: { name: 'pipelines-list', keepAlive: true }, jobsDetail: { name: 'jobs-detail', keepAlive: false }, diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue new file mode 100644 index 00000000000..777f8fa6691 --- /dev/null +++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue @@ -0,0 +1,101 @@ +<script> +import { mapActions, mapState, mapGetters } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { __, sprintf } from '~/locale'; +import ImportedProjectTableRow from './imported_project_table_row.vue'; +import ProviderRepoTableRow from './provider_repo_table_row.vue'; +import eventHub from '../event_hub'; + +export default { + name: 'ImportProjectsTable', + components: { + ImportedProjectTableRow, + ProviderRepoTableRow, + LoadingButton, + GlLoadingIcon, + }, + props: { + providerTitle: { + type: String, + required: true, + }, + }, + + computed: { + ...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos']), + ...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']), + + emptyStateText() { + return sprintf(__('No %{providerTitle} repositories available to import'), { + providerTitle: this.providerTitle, + }); + }, + + fromHeaderText() { + return sprintf(__('From %{providerTitle}'), { providerTitle: this.providerTitle }); + }, + }, + + mounted() { + return this.fetchRepos(); + }, + + beforeDestroy() { + this.stopJobsPolling(); + this.clearJobsEtagPoll(); + }, + + methods: { + ...mapActions(['fetchRepos', 'fetchJobs', 'stopJobsPolling', 'clearJobsEtagPoll']), + + importAll() { + eventHub.$emit('importAll'); + }, + }, +}; +</script> + +<template> + <div> + <div class="d-flex justify-content-between align-items-end flex-wrap mb-3"> + <p class="light text-nowrap mt-2 my-sm-0"> + {{ s__('ImportProjects|Select the projects you want to import') }} + </p> + <loading-button + container-class="btn btn-success js-import-all" + :loading="isImportingAnyRepo" + :label="__('Import all repositories')" + :disabled="!hasProviderRepos" + type="button" + @click="importAll" + /> + </div> + <gl-loading-icon + v-if="isLoadingRepos" + class="js-loading-button-icon import-projects-loading-icon" + :size="4" + /> + <div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive"> + <table class="table import-table"> + <thead> + <th class="import-jobs-from-col">{{ fromHeaderText }}</th> + <th class="import-jobs-to-col">{{ __('To GitLab') }}</th> + <th class="import-jobs-status-col">{{ __('Status') }}</th> + <th class="import-jobs-cta-col"></th> + </thead> + <tbody> + <imported-project-table-row + v-for="project in importedProjects" + :key="project.id" + :project="project" + /> + <provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" /> + </tbody> + </table> + </div> + <div v-else class="text-center"> + <strong>{{ emptyStateText }}</strong> + </div> + </div> +</template> diff --git a/app/assets/javascripts/import_projects/components/import_status.vue b/app/assets/javascripts/import_projects/components/import_status.vue new file mode 100644 index 00000000000..9e3347a657f --- /dev/null +++ b/app/assets/javascripts/import_projects/components/import_status.vue @@ -0,0 +1,47 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import STATUS_MAP from '../constants'; + +export default { + name: 'ImportStatus', + components: { + CiIcon, + GlLoadingIcon, + }, + props: { + status: { + type: String, + required: true, + }, + }, + + computed: { + mappedStatus() { + return STATUS_MAP[this.status]; + }, + + ciIconStatus() { + const { icon } = this.mappedStatus; + + return { + icon: `status_${icon}`, + group: icon, + }; + }, + }, +}; +</script> + +<template> + <div> + <gl-loading-icon + v-if="mappedStatus.loadingIcon" + :inline="true" + :class="mappedStatus.textClass" + class="align-middle mr-2" + /> + <ci-icon v-else css-classes="align-middle mr-2" :status="ciIconStatus" /> + <span :class="mappedStatus.textClass">{{ mappedStatus.text }}</span> + </div> +</template> diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue new file mode 100644 index 00000000000..ab2bd87ee9f --- /dev/null +++ b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue @@ -0,0 +1,55 @@ +<script> +import ImportStatus from './import_status.vue'; +import { STATUSES } from '../constants'; + +export default { + name: 'ImportedProjectTableRow', + components: { + ImportStatus, + }, + props: { + project: { + type: Object, + required: true, + }, + }, + + computed: { + displayFullPath() { + return this.project.fullPath.replace(/^\//, ''); + }, + + isFinished() { + return this.project.importStatus === STATUSES.FINISHED; + }, + }, +}; +</script> + +<template> + <tr class="js-imported-project import-row"> + <td> + <a + :href="project.providerLink" + rel="noreferrer noopener" + target="_blank" + class="js-provider-link" + > + {{ project.importSource }} + </a> + </td> + <td class="js-full-path">{{ displayFullPath }}</td> + <td><import-status :status="project.importStatus" /></td> + <td> + <a + v-if="isFinished" + class="btn btn-default js-go-to-project" + :href="project.fullPath" + rel="noreferrer noopener" + target="_blank" + > + {{ __('Go to project') }} + </a> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue new file mode 100644 index 00000000000..7cc29fa1b91 --- /dev/null +++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue @@ -0,0 +1,110 @@ +<script> +import { mapState, mapGetters, mapActions } from 'vuex'; +import Select2Select from '~/vue_shared/components/select2_select.vue'; +import { __ } from '~/locale'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import eventHub from '../event_hub'; +import { STATUSES } from '../constants'; +import ImportStatus from './import_status.vue'; + +export default { + name: 'ProviderRepoTableRow', + components: { + Select2Select, + LoadingButton, + ImportStatus, + }, + props: { + repo: { + type: Object, + required: true, + }, + }, + + data() { + return { + targetNamespace: this.$store.state.defaultTargetNamespace, + newName: this.repo.sanitizedName, + }; + }, + + computed: { + ...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']), + + ...mapGetters(['namespaceSelectOptions']), + + importButtonText() { + return this.ciCdOnly ? __('Connect') : __('Import'); + }, + + select2Options() { + return { + data: this.namespaceSelectOptions, + containerCssClass: + 'import-namespace-select js-namespace-select qa-project-namespace-select', + }; + }, + + isLoadingImport() { + return this.reposBeingImported.includes(this.repo.id); + }, + + status() { + return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE; + }, + }, + + created() { + eventHub.$on('importAll', () => this.importRepo()); + }, + + methods: { + ...mapActions(['fetchImport']), + + importRepo() { + return this.fetchImport({ + newName: this.newName, + targetNamespace: this.targetNamespace, + repo: this.repo, + }); + }, + }, +}; +</script> + +<template> + <tr class="qa-project-import-row js-provider-repo import-row"> + <td> + <a + :href="repo.providerLink" + rel="noreferrer noopener" + target="_blank" + class="js-provider-link" + > + {{ repo.fullName }} + </a> + </td> + <td class="d-flex flex-wrap flex-lg-nowrap"> + <select2-select v-model="targetNamespace" :options="select2Options" /> + <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center" + >/</span + > + <input + v-model="newName" + type="text" + class="form-control import-project-name-input js-new-name qa-project-path-field" + /> + </td> + <td><import-status :status="status" /></td> + <td> + <button + v-if="!isLoadingImport" + type="button" + class="qa-import-button js-import-button btn btn-default" + @click="importRepo" + > + {{ importButtonText }} + </button> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/import_projects/constants.js b/app/assets/javascripts/import_projects/constants.js new file mode 100644 index 00000000000..ad33ca158d2 --- /dev/null +++ b/app/assets/javascripts/import_projects/constants.js @@ -0,0 +1,48 @@ +import { __ } from '../locale'; + +// The `scheduling` status is only present on the client-side, +// it is used as the status when we are requesting to start an import. + +export const STATUSES = { + FINISHED: 'finished', + FAILED: 'failed', + SCHEDULED: 'scheduled', + STARTED: 'started', + NONE: 'none', + SCHEDULING: 'scheduling', +}; + +const STATUS_MAP = { + [STATUSES.FINISHED]: { + icon: 'success', + text: __('Done'), + textClass: 'text-success', + }, + [STATUSES.FAILED]: { + icon: 'failed', + text: __('Failed'), + textClass: 'text-danger', + }, + [STATUSES.SCHEDULED]: { + icon: 'pending', + text: __('Scheduled'), + textClass: 'text-warning', + }, + [STATUSES.STARTED]: { + icon: 'running', + text: __('Running…'), + textClass: 'text-info', + }, + [STATUSES.NONE]: { + icon: 'created', + text: __('Not started'), + textClass: 'text-muted', + }, + [STATUSES.SCHEDULING]: { + loadingIcon: true, + text: __('Scheduling'), + textClass: 'text-warning', + }, +}; + +export default STATUS_MAP; diff --git a/app/assets/javascripts/import_projects/event_hub.js b/app/assets/javascripts/import_projects/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/import_projects/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js new file mode 100644 index 00000000000..5c77484aee1 --- /dev/null +++ b/app/assets/javascripts/import_projects/index.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import { mapActions } from 'vuex'; +import Translate from '../vue_shared/translate'; +import ImportProjectsTable from './components/import_projects_table.vue'; +import { parseBoolean } from '../lib/utils/common_utils'; +import store from './store'; + +Vue.use(Translate); + +export default function mountImportProjectsTable(mountElement) { + if (!mountElement) return undefined; + + const { + reposPath, + provider, + providerTitle, + canSelectNamespace, + jobsPath, + importPath, + ciCdOnly, + } = mountElement.dataset; + + return new Vue({ + el: mountElement, + store, + + created() { + this.setInitialData({ + reposPath, + provider, + jobsPath, + importPath, + defaultTargetNamespace: gon.current_username, + ciCdOnly: parseBoolean(ciCdOnly), + canSelectNamespace: parseBoolean(canSelectNamespace), + }); + }, + + methods: { + ...mapActions(['setInitialData']), + }, + + render(createElement) { + return createElement(ImportProjectsTable, { props: { providerTitle } }); + }, + }); +} diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js new file mode 100644 index 00000000000..c44500937cc --- /dev/null +++ b/app/assets/javascripts/import_projects/store/actions.js @@ -0,0 +1,106 @@ +import Visibility from 'visibilityjs'; +import * as types from './mutation_types'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import Poll from '~/lib/utils/poll'; +import createFlash from '~/flash'; +import { s__, sprintf } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; + +let eTagPoll; + +export const clearJobsEtagPoll = () => { + eTagPoll = null; +}; +export const stopJobsPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; +export const restartJobsPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); + +export const requestRepos = ({ commit }, repos) => commit(types.REQUEST_REPOS, repos); +export const receiveReposSuccess = ({ commit }, repos) => + commit(types.RECEIVE_REPOS_SUCCESS, repos); +export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR); +export const fetchRepos = ({ state, dispatch }) => { + dispatch('requestRepos'); + + return axios + .get(state.reposPath) + .then(({ data }) => + dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })), + ) + .then(() => dispatch('fetchJobs')) + .catch(() => { + createFlash( + sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), { + provider: state.provider, + }), + ); + + dispatch('receiveReposError'); + }); +}; + +export const requestImport = ({ commit, state }, repoId) => { + if (!state.reposBeingImported.includes(repoId)) commit(types.REQUEST_IMPORT, repoId); +}; +export const receiveImportSuccess = ({ commit }, { importedProject, repoId }) => + commit(types.RECEIVE_IMPORT_SUCCESS, { importedProject, repoId }); +export const receiveImportError = ({ commit }, repoId) => + commit(types.RECEIVE_IMPORT_ERROR, repoId); +export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, repo }) => { + dispatch('requestImport', repo.id); + + return axios + .post(state.importPath, { + ci_cd_only: state.ciCdOnly, + new_name: newName, + repo_id: repo.id, + target_namespace: targetNamespace, + }) + .then(({ data }) => + dispatch('receiveImportSuccess', { + importedProject: convertObjectPropsToCamelCase(data, { deep: true }), + repoId: repo.id, + }), + ) + .catch(() => { + createFlash(s__('ImportProjects|Importing the project failed')); + + dispatch('receiveImportError', { repoId: repo.id }); + }); +}; + +export const receiveJobsSuccess = ({ commit }, updatedProjects) => + commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects); +export const fetchJobs = ({ state, dispatch }) => { + if (eTagPoll) return; + + eTagPoll = new Poll({ + resource: { + fetchJobs: () => axios.get(state.jobsPath), + }, + method: 'fetchJobs', + successCallback: ({ data }) => + dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })), + errorCallback: () => createFlash(s__('ImportProjects|Updating the imported projects failed')), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartJobsPolling'); + } else { + dispatch('stopJobsPolling'); + } + }); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js new file mode 100644 index 00000000000..f03474a8404 --- /dev/null +++ b/app/assets/javascripts/import_projects/store/getters.js @@ -0,0 +1,20 @@ +export const namespaceSelectOptions = state => { + const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({ + id: fullPath, + text: fullPath, + })); + + return [ + { text: 'Groups', children: serializedNamespaces }, + { + text: 'Users', + children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }], + }, + ]; +}; + +export const isImportingAnyRepo = state => state.reposBeingImported.length > 0; + +export const hasProviderRepos = state => state.providerRepos.length > 0; + +export const hasImportedProjects = state => state.importedProjects.length > 0; diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js new file mode 100644 index 00000000000..6ac9bfd8189 --- /dev/null +++ b/app/assets/javascripts/import_projects/store/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: state(), + actions, + mutations, + getters, +}); diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js new file mode 100644 index 00000000000..6ba3fd6f29e --- /dev/null +++ b/app/assets/javascripts/import_projects/store/mutation_types.js @@ -0,0 +1,11 @@ +export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; + +export const REQUEST_REPOS = 'REQUEST_REPOS'; +export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS'; +export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR'; + +export const REQUEST_IMPORT = 'REQUEST_IMPORT'; +export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS'; +export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR'; + +export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js new file mode 100644 index 00000000000..b88de0268e7 --- /dev/null +++ b/app/assets/javascripts/import_projects/store/mutations.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; +import * as types from './mutation_types'; + +export default { + [types.SET_INITIAL_DATA](state, data) { + Object.assign(state, data); + }, + + [types.REQUEST_REPOS](state) { + state.isLoadingRepos = true; + }, + + [types.RECEIVE_REPOS_SUCCESS](state, { importedProjects, providerRepos, namespaces }) { + state.isLoadingRepos = false; + + state.importedProjects = importedProjects; + state.providerRepos = providerRepos; + state.namespaces = namespaces; + }, + + [types.RECEIVE_REPOS_ERROR](state) { + state.isLoadingRepos = false; + }, + + [types.REQUEST_IMPORT](state, repoId) { + state.reposBeingImported.push(repoId); + }, + + [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) { + const existingRepoIndex = state.reposBeingImported.indexOf(repoId); + if (state.reposBeingImported.includes(repoId)) + state.reposBeingImported.splice(existingRepoIndex, 1); + + const providerRepoIndex = state.providerRepos.findIndex( + providerRepo => providerRepo.id === repoId, + ); + state.providerRepos.splice(providerRepoIndex, 1); + state.importedProjects.unshift(importedProject); + }, + + [types.RECEIVE_IMPORT_ERROR](state, repoId) { + const repoIndex = state.reposBeingImported.indexOf(repoId); + if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1); + }, + + [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) { + updatedProjects.forEach(updatedProject => { + const existingProject = state.importedProjects.find( + importedProject => importedProject.id === updatedProject.id, + ); + + Vue.set(existingProject, 'importStatus', updatedProject.importStatus); + }); + }, +}; diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js new file mode 100644 index 00000000000..637fef6e53c --- /dev/null +++ b/app/assets/javascripts/import_projects/store/state.js @@ -0,0 +1,15 @@ +export default () => ({ + reposPath: '', + importPath: '', + jobsPath: '', + currentProjectId: '', + provider: '', + currentUsername: '', + importedProjects: [], + providerRepos: [], + namespaces: [], + reposBeingImported: [], + isLoadingRepos: false, + canSelectNamespace: false, + ciCdOnly: false, +}); diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index a2141dc3760..1691ac62100 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -110,7 +110,7 @@ export default { <div class="sidebar-container"> <div class="blocks-container"> <div class="block d-flex flex-nowrap align-items-center"> - <h4 class="my-0 mr-2">{{ job.name }}</h4> + <h4 class="my-0 mr-2 text-break-word">{{ job.name }}</h4> <div class="flex-grow-1 flex-shrink-0 text-right"> <gl-link v-if="job.retry_path" diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 0ceff10a02a..29fe460017e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -130,7 +130,7 @@ export const isInViewport = (el, offset = {}) => { rect.top >= (top || 0) && rect.left >= (left || 0) && rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth + parseInt(rect.right, 10) <= window.innerWidth ); }; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 376d4114efd..d8947e8ca50 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -5,6 +5,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; import { GlSkeletonLoading } from '@gitlab/ui'; import { getDiffMode } from '~/diffs/store/utils'; +import { diffViewerModes } from '~/ide/constants'; export default { components: { @@ -31,6 +32,12 @@ export default { diffMode() { return getDiffMode(this.discussion.diff_file); }, + diffViewerMode() { + return this.discussion.diff_file.viewer.name; + }, + isTextFile() { + return this.diffViewerMode === diffViewerModes.text; + }, hasTruncatedDiffLines() { return ( this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0 @@ -58,18 +65,14 @@ export default { </script> <template> - <div :class="{ 'text-file': discussion.diff_file.text }" class="diff-file file-holder"> + <div :class="{ 'text-file': isTextFile }" class="diff-file file-holder"> <diff-file-header :discussion-path="discussion.discussion_path" :diff-file="discussion.diff_file" :can-current-user-fork="false" - :expanded="!discussion.diff_file.collapsed" + :expanded="!discussion.diff_file.viewer.collapsed" /> - <div - v-if="discussion.diff_file.text" - :class="$options.userColorSchemeClass" - class="diff-content code" - > + <div v-if="isTextFile" :class="$options.userColorSchemeClass" class="diff-content code"> <table> <template v-if="hasTruncatedDiffLines"> <tr @@ -109,6 +112,7 @@ export default { <div v-else> <diff-viewer :diff-mode="diffMode" + :diff-viewer-mode="diffViewerMode" :new-path="discussion.diff_file.new_path" :new-sha="discussion.diff_file.diff_refs.head_sha" :old-path="discussion.diff_file.old_path" diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 91b9e5de374..de1ea0f58d6 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -23,11 +23,6 @@ export default { type: [String, Number], required: true, }, - discussionId: { - type: String, - required: false, - default: '', - }, noteUrl: { type: String, required: false, @@ -126,6 +121,11 @@ export default { onResolve() { this.$emit('handleResolve'); }, + closeTooltip() { + this.$nextTick(() => { + this.$root.$emit('bv::hide::tooltip'); + }); + }, }, }; </script> @@ -171,7 +171,7 @@ export default { v-if="showReplyButton" ref="replyButton" class="js-reply-button" - :note-id="discussionId" + @startReplying="$emit('startReplying')" /> <div v-if="canEdit" class="note-actions-item"> <button @@ -202,6 +202,7 @@ export default { title="More actions" class="note-action-button more-actions-toggle btn btn-transparent" data-toggle="dropdown" + @click="closeTooltip" > <icon css-classes="icon" name="ellipsis_v" /> </button> diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index b2f9d7f128a..f50cab81efe 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -1,5 +1,4 @@ <script> -import { mapActions } from 'vuex'; import { GlTooltipDirective, GlButton } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; @@ -12,15 +11,6 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - props: { - noteId: { - type: String, - required: true, - }, - }, - methods: { - ...mapActions(['convertToDiscussion']), - }, }; </script> @@ -32,7 +22,7 @@ export default { class="note-action-button" variant="transparent" :title="__('Reply to comment')" - @click="convertToDiscussion(noteId)" + @click="$emit('startReplying')" > <icon name="comment" css-classes="link-highlight" /> </gl-button> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index ff303d0f55a..fb1d98355b3 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -95,6 +95,7 @@ export default { <div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body"> <suggestions v-if="hasSuggestion && !isEditing" + class="note-text md" :suggestions="note.suggestions" :note-html="note.note_html" :line-type="lineType" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index b7e9f7c2028..2d6fd8b116f 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -26,6 +26,7 @@ import resolvable from '../mixins/resolvable'; import discussionNavigation from '../mixins/discussion_navigation'; import ReplyPlaceholder from './discussion_reply_placeholder.vue'; import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; +import eventHub from '../event_hub'; export default { name: 'NoteableDiscussion', @@ -93,6 +94,7 @@ export default { }, computed: { ...mapGetters([ + 'convertedDisscussionIds', 'getNoteableData', 'nextUnresolvedDiscussionId', 'unresolvedDiscussionsCount', @@ -245,6 +247,12 @@ export default { } }, }, + created() { + eventHub.$on('startReplying', this.onStartReplying); + }, + beforeDestroy() { + eventHub.$off('startReplying', this.onStartReplying); + }, methods: { ...mapActions([ 'saveNote', @@ -252,6 +260,7 @@ export default { 'removePlaceholderNotes', 'toggleResolveNote', 'expandDiscussion', + 'removeConvertedDiscussion', ]), truncateSha, componentName(note) { @@ -291,6 +300,10 @@ export default { } } + if (this.convertedDisscussionIds.includes(this.discussion.id)) { + this.removeConvertedDiscussion(this.discussion.id); + } + this.isReplying = false; this.resetAutoSave(); }, @@ -301,6 +314,10 @@ export default { note: { note: noteText }, }; + if (this.convertedDisscussionIds.includes(this.discussion.id)) { + postData.return_discussion = true; + } + if (this.discussion.for_commit) { postData.note_project_id = this.discussion.project_id; } @@ -340,6 +357,11 @@ Please check your network connection and try again.`; deleteNoteHandler(note) { this.$emit('noteDeleted', this.discussion, note); }, + onStartReplying(discussionId) { + if (this.discussion.id === discussionId) { + this.showReplyForm(); + } + }, }, }; </script> @@ -358,30 +380,32 @@ Please check your network connection and try again.`; :img-size="40" /> </div> - <note-header - :author="author" - :created-at="initialDiscussion.created_at" - :note-id="initialDiscussion.id" - :include-toggle="true" - :expanded="discussion.expanded" - @toggleHandler="toggleDiscussionHandler" - > - <span v-html="actionText"></span> - </note-header> - <note-edited-text - v-if="discussion.resolved" - :edited-at="discussion.resolved_at" - :edited-by="discussion.resolved_by" - :action-text="resolvedText" - class-name="discussion-headline-light js-discussion-headline" - /> - <note-edited-text - v-else-if="lastUpdatedAt" - :edited-at="lastUpdatedAt" - :edited-by="lastUpdatedBy" - action-text="Last updated" - class-name="discussion-headline-light js-discussion-headline" - /> + <div class="timeline-content"> + <note-header + :author="author" + :created-at="initialDiscussion.created_at" + :note-id="initialDiscussion.id" + :include-toggle="true" + :expanded="discussion.expanded" + @toggleHandler="toggleDiscussionHandler" + > + <span v-html="actionText"></span> + </note-header> + <note-edited-text + v-if="discussion.resolved" + :edited-at="discussion.resolved_at" + :edited-by="discussion.resolved_by" + :action-text="resolvedText" + class-name="discussion-headline-light js-discussion-headline" + /> + <note-edited-text + v-else-if="lastUpdatedAt" + :edited-at="lastUpdatedAt" + :edited-by="lastUpdatedBy" + action-text="Last updated" + class-name="discussion-headline-light js-discussion-headline" + /> + </div> </div> <div v-if="shouldShowDiscussions" class="discussion-body"> <component @@ -400,6 +424,7 @@ Please check your network connection and try again.`; :help-page-path="helpPagePath" :show-reply-button="canReply" @handleDeleteNote="deleteNoteHandler" + @startReplying="showReplyForm" > <note-edited-text v-if="discussion.resolved" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 56108a58010..04e74a43acc 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -29,11 +29,6 @@ export default { type: Object, required: true, }, - discussion: { - type: Object, - required: false, - default: null, - }, line: { type: Object, required: false, @@ -49,6 +44,11 @@ export default { required: false, default: () => null, }, + showReplyButton: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -91,13 +91,6 @@ export default { } return ''; }, - showReplyButton() { - if (!this.discussion || !this.getNoteableData.current_user.can_create_note) { - return false; - } - - return this.discussion.individual_note && !this.commentsDisabled; - }, actionText() { if (!this.commit) { return ''; @@ -260,10 +253,10 @@ export default { :is-resolved="note.resolved" :is-resolving="isResolving" :resolved-by="note.resolved_by" - :discussion-id="discussionId" @handleEdit="editHandler" @handleDelete="deleteHandler" @handleResolve="resolveHandler" + @startReplying="$emit('startReplying')" /> </div> <div class="timeline-discussion-body"> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 6d72b72e628..a63571edcea 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -60,9 +60,11 @@ export default { ...mapGetters([ 'isNotesFetched', 'discussions', + 'convertedDisscussionIds', 'getNotesDataByProp', 'isLoading', 'commentsDisabled', + 'getNoteableData', ]), noteableType() { return this.noteableData.noteableType; @@ -78,6 +80,9 @@ export default { return this.discussions; }, + canReply() { + return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled; + }, }, watch: { shouldShow() { @@ -128,6 +133,7 @@ export default { 'setNotesFetchedState', 'expandDiscussion', 'startTaskList', + 'convertToDiscussion', ]), fetchNotes() { if (this.isFetching) return null; @@ -175,6 +181,11 @@ export default { } } }, + startReplying(discussionId) { + return this.convertToDiscussion(discussionId) + .then(() => this.$nextTick()) + .then(() => eventHub.$emit('startReplying', discussionId)); + }, }, systemNote: constants.SYSTEM_NOTE, }; @@ -193,7 +204,9 @@ export default { /> <placeholder-note v-else :key="discussion.id" :note="discussion.notes[0]" /> </template> - <template v-else-if="discussion.individual_note"> + <template + v-else-if="discussion.individual_note && !convertedDisscussionIds.includes(discussion.id)" + > <system-note v-if="discussion.notes[0].system" :key="discussion.id" @@ -203,7 +216,8 @@ export default { v-else :key="discussion.id" :note="discussion.notes[0]" - :discussion="discussion" + :show-reply-button="canReply" + @startReplying="startReplying(discussion.id)" /> </template> <noteable-discussion diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index ff65f14d529..1a0dba69a7c 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -83,12 +83,44 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) => dispatch('startTaskList'); }); -export const replyToDiscussion = ({ commit }, { endpoint, data }) => +export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => { + const { notesById } = getters; + + notes.forEach(note => { + if (notesById[note.id]) { + commit(types.UPDATE_NOTE, note); + } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { + const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id); + + if (discussion) { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); + } else if (note.type === constants.DIFF_NOTE) { + dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); + } else { + commit(types.ADD_NEW_NOTE, note); + } + } else { + commit(types.ADD_NEW_NOTE, note); + } + }); +}; + +export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoint, data }) => service .replyToDiscussion(endpoint, data) .then(res => res.json()) .then(res => { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); + if (res.discussion) { + commit(types.UPDATE_DISCUSSION, res.discussion); + + updateOrCreateNotes({ commit, state, getters, dispatch }, res.discussion.notes); + + dispatch('updateMergeRequestWidget'); + dispatch('startTaskList'); + dispatch('updateResolvableDiscussonsCounts'); + } else { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); + } return res; }); @@ -262,25 +294,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { if (resp.notes && resp.notes.length) { - const { notesById } = getters; - - resp.notes.forEach(note => { - if (notesById[note.id]) { - commit(types.UPDATE_NOTE, note); - } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { - const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id); - - if (discussion) { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); - } else if (note.type === constants.DIFF_NOTE) { - dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); - } else { - commit(types.ADD_NEW_NOTE, note); - } - } else { - commit(types.ADD_NEW_NOTE, note); - } - }); + updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes); dispatch('startTaskList'); } @@ -429,5 +443,8 @@ export const submitSuggestion = ( export const convertToDiscussion = ({ commit }, noteId) => commit(types.CONVERT_TO_DISCUSSION, noteId); +export const removeConvertedDiscussion = ({ commit }, noteId) => + commit(types.REMOVE_CONVERTED_DISCUSSION, noteId); + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 0ffc0cb2593..5026c13dab5 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -4,6 +4,8 @@ import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => collapseSystemNotes(state.discussions); +export const convertedDisscussionIds = state => state.convertedDisscussionIds; + export const targetNoteHash = state => state.targetNoteHash; export const getNotesData = state => state.notesData; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 887e6d22b06..6168aeae35d 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -5,6 +5,7 @@ import mutations from '../mutations'; export default () => ({ state: { discussions: [], + convertedDisscussionIds: [], targetNoteHash: null, lastFetchedAt: null, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 2bffedad336..796370920bb 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -18,6 +18,7 @@ export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; export const APPLY_SUGGESTION = 'APPLY_SUGGESTION'; export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION'; +export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index d167f8ef421..ae6f8b7790a 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -266,7 +266,14 @@ export default { }, [types.CONVERT_TO_DISCUSSION](state, discussionId) { - const discussion = utils.findNoteObjectById(state.discussions, discussionId); - Object.assign(discussion, { individual_note: false }); + const convertedDisscussionIds = [...state.convertedDisscussionIds, discussionId]; + Object.assign(state, { convertedDisscussionIds }); + }, + + [types.REMOVE_CONVERTED_DISCUSSION](state, discussionId) { + const convertedDisscussionIds = [...state.convertedDisscussionIds]; + + convertedDisscussionIds.splice(convertedDisscussionIds.indexOf(discussionId), 1); + Object.assign(state, { convertedDisscussionIds }); }, }; diff --git a/app/assets/javascripts/pages/import/gitea/status/index.js b/app/assets/javascripts/pages/import/gitea/status/index.js new file mode 100644 index 00000000000..dcd84f0faf9 --- /dev/null +++ b/app/assets/javascripts/pages/import/gitea/status/index.js @@ -0,0 +1,7 @@ +import mountImportProjectsTable from '~/import_projects'; + +document.addEventListener('DOMContentLoaded', () => { + const mountElement = document.getElementById('import-projects-mount-element'); + + mountImportProjectsTable(mountElement); +}); diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js new file mode 100644 index 00000000000..dcd84f0faf9 --- /dev/null +++ b/app/assets/javascripts/pages/import/github/status/index.js @@ -0,0 +1,7 @@ +import mountImportProjectsTable from '~/import_projects'; + +document.addEventListener('DOMContentLoaded', () => { + const mountElement = document.getElementById('import-projects-mount-element'); + + mountImportProjectsTable(mountElement); +}); diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js index d54bff88f70..e1a3f42a71f 100644 --- a/app/assets/javascripts/pages/sessions/new/index.js +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import UsernameValidator from './username_validator'; +import NoEmojiValidator from '../../../emoji/no_emoji_validator'; import SigninTabsMemoizer from './signin_tabs_memoizer'; import OAuthRememberMe from './oauth_remember_me'; import preserveUrlFragment from './preserve_url_fragment'; @@ -7,6 +8,7 @@ import preserveUrlFragment from './preserve_url_fragment'; document.addEventListener('DOMContentLoaded', () => { new UsernameValidator(); // eslint-disable-line no-new new SigninTabsMemoizer(); // eslint-disable-line no-new + new NoEmojiValidator(); // eslint-disable-line no-new new OAuthRememberMe({ container: $('.omniauth-container'), diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 0152e2fbe04..a250e3236f5 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -59,17 +59,19 @@ export default { </script> <template> <div class="btn-group"> - <gl-button + <button v-gl-tooltip + type="button" :disabled="isLoading" class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" - title="Manual job" + :title="__('Manual job')" data-toggle="dropdown" - aria-label="Manual job" + :aria-label="__('Manual job')" > - <icon name="play" class="icon-play" /> <i class="fa fa-caret-down" aria-hidden="true"> </i> + <icon name="play" class="icon-play" /> + <i class="fa fa-caret-down" aria-hidden="true"></i> <gl-loading-icon v-if="isLoading" /> - </gl-button> + </button> <ul class="dropdown-menu dropdown-menu-right"> <li v-for="action in actions" :key="action.path"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 908b10afee6..2ab0ad4d013 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,5 +1,5 @@ <script> -import { GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; export default { @@ -9,7 +9,6 @@ export default { components: { Icon, GlLink, - GlButton, }, props: { artifacts: { @@ -21,20 +20,22 @@ export default { </script> <template> <div class="btn-group" role="group"> - <gl-button + <button v-gl-tooltip - class="dropdown-toggle build-artifacts js-pipeline-dropdown-download" - title="Artifacts" + type="button" + class="dropdown-toggle build-artifacts btn btn-default js-pipeline-dropdown-download" + :title="__('Artifacts')" data-toggle="dropdown" - aria-label="Artifacts" + :aria-label="__('Artifacts')" > - <icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"> </i> - </gl-button> + <icon name="download" /> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(artifact, i) in artifacts" :key="i"> - <gl-link :href="artifact.path" rel="nofollow" download> - Download {{ artifact.name }} artifacts - </gl-link> + <gl-link :href="artifact.path" rel="nofollow" download + >Download {{ artifact.name }} artifacts</gl-link + > </li> </ul> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue index a1d3a09cca4..33963d5e1e6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -73,14 +73,14 @@ export default { <gl-button :aria-label="ariaLabel" variant="blank" - class="commit-edit-toggle mr-2" + class="commit-edit-toggle square s24 mr-2" @click.stop="toggle()" > <icon :name="collapseIcon" :size="16" /> </gl-button> <span v-if="expanded">{{ __('Collapse') }}</span> <span v-else> - <span v-html="message"></span> + <span class="vertical-align-middle" v-html="message"></span> <gl-button variant="link" class="modify-message-button"> {{ modifyLinkMessage }} </gl-button> diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index bb7710f708e..e9ab6f5ba7a 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -37,6 +37,11 @@ export default { required: false, default: 12, }, + isCentered: { + type: Boolean, + required: false, + default: true, + }, }, computed: { changedIcon() { @@ -78,7 +83,12 @@ export default { </script> <template> - <span v-gl-tooltip.right :title="tooltipTitle" class="file-changed-icon ml-auto"> + <span + v-gl-tooltip.right + :title="tooltipTitle" + :class="{ 'ml-auto': isCentered }" + class="file-changed-icon" + > <icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index b8eb555106f..2f498c4fa2a 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -46,6 +46,11 @@ export default { required: false, default: false, }, + cssClasses: { + type: String, + required: false, + default: '', + }, }, computed: { cssClass() { @@ -59,5 +64,5 @@ export default { }; </script> <template> - <span :class="cssClass"> <icon :name="icon" :size="size" /> </span> + <span :class="cssClass"> <icon :name="icon" :size="size" :css-classes="cssClasses" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue index 75c66ed850b..ebb253ff422 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -1,6 +1,5 @@ <script> -import { diffModes } from '~/ide/constants'; -import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils'; +import { diffViewerModes, diffModes } from '~/ide/constants'; import ImageDiffViewer from './viewers/image_diff_viewer.vue'; import DownloadDiffViewer from './viewers/download_diff_viewer.vue'; import RenamedFile from './viewers/renamed.vue'; @@ -12,6 +11,10 @@ export default { type: String, required: true, }, + diffViewerMode: { + type: String, + required: true, + }, newPath: { type: String, required: true, @@ -46,7 +49,7 @@ export default { }, computed: { viewer() { - if (this.diffMode === diffModes.renamed) { + if (this.diffViewerMode === diffViewerModes.renamed) { return RenamedFile; } else if (this.diffMode === diffModes.mode_changed) { return ModeChanged; @@ -54,11 +57,8 @@ export default { if (!this.newPath) return null; - const previewInfo = viewerInformationForPath(this.newPath); - if (!previewInfo) return DownloadDiffViewer; - - switch (previewInfo.id) { - case 'image': + switch (this.diffViewerMode) { + case diffViewerModes.image: return ImageDiffViewer; default: return DownloadDiffViewer; diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue new file mode 100644 index 00000000000..c5cdddf2f64 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/no_preview.vue @@ -0,0 +1,5 @@ +<template> + <div class="nothing-here-block"> + {{ __('No preview for this file type') }} + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue new file mode 100644 index 00000000000..d4d3038f066 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue @@ -0,0 +1,5 @@ +<template> + <div class="nothing-here-block"> + {{ __('This diff was suppressed by a .gitattributes entry.') }} + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index f54033efc54..0cbcdbf2eb4 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -136,6 +136,7 @@ export default { <div v-else :class="fileClass" + :title="file.name" class="file-row" role="button" @click="clickFile" diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index c33665c24f6..dcda701f049 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -130,6 +130,6 @@ export default { <template> <div> <div class="flash-container js-suggestions-flash"></div> - <div v-show="isRendered" ref="container" class="note-text md" v-html="noteHtml"></div> + <div v-show="isRendered" ref="container" v-html="noteHtml"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index bf736a378dd..8d81940eb91 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -28,11 +28,12 @@ export default { data() { return { size: this.startSize, + isDragging: false, }; }, computed: { className() { - return `drag-${this.side}`; + return [`position-${this.side}-0`, { 'is-dragging': this.isDragging }]; }, cursorStyle() { if (this.enabled) { @@ -57,6 +58,7 @@ export default { startDrag(e) { if (this.enabled) { e.preventDefault(); + this.isDragging = true; this.startPos = e.clientX; this.currentStartSize = this.size; document.addEventListener('mousemove', this.drag); @@ -80,6 +82,7 @@ export default { }, endDrag(e) { e.preventDefault(); + this.isDragging = false; document.removeEventListener('mousemove', this.drag); this.$emit('resize-end', this.size); }, @@ -91,7 +94,7 @@ export default { <div :class="className" :style="cursorStyle" - class="drag-handle" + class="position-absolute position-top-0 position-bottom-0 drag-handle" @mousedown="startDrag" @dblclick="resetSize" ></div> diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue new file mode 100644 index 00000000000..19c5da0461a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/select2_select.vue @@ -0,0 +1,34 @@ +<script> +import $ from 'jquery'; + +export default { + name: 'Select2Select', + props: { + options: { + type: Object, + required: false, + default: () => ({}), + }, + value: { + type: String, + required: false, + default: '', + }, + }, + + mounted() { + $(this.$refs.dropdownInput) + .val(this.value) + .select2(this.options) + .on('change', event => this.$emit('input', event.target.value)); + }, + + beforeDestroy() { + $(this.$refs.dropdownInput).select2('destroy'); + }, +}; +</script> + +<template> + <input ref="dropdownInput" type="hidden" /> +</template> diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 4fb787887a1..70d50c74ca9 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -63,15 +63,15 @@ // // Pass in any number of transitions @mixin transition($transitions...) { - $unfoldedTransitions: (); + $unfolded-transitions: (); @each $transition in $transitions { - $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma); + $unfolded-transitions: append($unfolded-transitions, unfold-transition($transition), comma); } - transition: $unfoldedTransitions; + transition: $unfolded-transitions; } -@mixin disableAllAnimation { +@mixin disable-all-animation { /*CSS transitions*/ -o-transition-property: none !important; -moz-transition-property: none !important; @@ -92,27 +92,27 @@ animation: none !important; } -@function unfoldTransition ($transition) { +@function unfold-transition ($transition) { // Default values $property: all; $duration: $general-hover-transition-duration; $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want $delay: null; // Browser default is 0, which is what we want - $defaultProperties: ($property, $duration, $easing, $delay); + $default-properties: ($property, $duration, $easing, $delay); // Grab transition properties if they exist - $unfoldedTransition: (); - @for $i from 1 through length($defaultProperties) { + $unfolded-transition: (); + @for $i from 1 through length($default-properties) { $p: null; @if $i <= length($transition) { $p: nth($transition, $i); } @else { - $p: nth($defaultProperties, $i); + $p: nth($default-properties, $i); } - $unfoldedTransition: append($unfoldedTransition, $p); + $unfolded-transition: append($unfolded-transition, $p); } - @return $unfoldedTransition; + @return $unfolded-transition; } .btn { diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index ad650d45314..5cfd5bbd4f5 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -15,7 +15,7 @@ margin-top: 3px; padding: $gl-padding; z-index: 300; - width: 300px; + width: $award-emoji-width; font-size: 14px; background-color: $white-light; border: 1px solid $border-white-light; @@ -55,6 +55,10 @@ transform: none; } } + + @include media-breakpoint-down(xs) { + width: $award-emoji-width-xs; + } } .emoji-search { @@ -229,10 +233,10 @@ height: $default-icon-size; width: $default-icon-size; border-radius: 50%; + } - path { - fill: $border-gray-normal; - } + path { + fill: $border-gray-normal; } } @@ -243,6 +247,10 @@ left: 10px; bottom: 6px; opacity: 0; + + path { + fill: $award-emoji-positive-add-lines; + } } .award-control-text { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index d164cc56e44..cb2c8879c5f 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -166,7 +166,8 @@ @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700); } - &.btn-remove { + &.btn-remove, + &.btn-danger { @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index c5c3b66438c..c1f2f5f8c6a 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -48,6 +48,10 @@ color: $brand-info; } +.text-break-word { + word-break: break-all; +} + .hint { font-style: italic; color: $gl-gray-400; } .light { color: $gl-text-color; } @@ -442,3 +446,15 @@ img.emoji { .position-left-0 { left: 0; } .position-right-0 { right: 0; } .position-top-0 { top: 0; } + +.drag-handle { + width: 4px; + + &:hover { + background-color: $white-normal; + } + + &.is-dragging { + background-color: $gray-600; + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 36dd1cee4de..23dcc1817b1 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -565,15 +565,14 @@ } .navbar-empty { + justify-content: center; height: $header-height; background: $white-light; border-bottom: 1px solid $white-normal; - .mx-auto { - .tanuki-logo, - img { - height: 36px; - } + .tanuki-logo, + .brand-header-logo { + max-height: 100%; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index f708a26bb32..961de8402ef 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -228,7 +228,7 @@ .cur { .avatar { - @include disableAllAnimation; + @include disable-all-animation; border: 1px solid $white-light; } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 9837b1a6bd0..b9d0c0d4d96 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -36,10 +36,6 @@ width: fit-content; } - tbody { - background-color: $white-light; - } - tr { th { border-bottom: solid 2px $gl-gray-100; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index ace46e32b18..3703b7568c8 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -111,10 +111,11 @@ body.modal-open { flex-grow: 1; height: 56px; padding: $gl-btn-padding $gl-btn-padding 0; + text-align: right; - > svg { - float: right; - height: 100%; + .illustration { + height: inherit; + width: initial; } } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index a08639936c0..bf85acdc0d6 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -49,13 +49,6 @@ word-wrap: normal; } - // Multi-line code blocks should scroll horizontally - pre { - code { - white-space: pre; - } - } - kbd { display: inline-block; padding: 3px 5px; @@ -166,6 +159,10 @@ overflow-x: auto; border-radius: 2px; + // Multi-line code blocks should scroll horizontally + code { + white-space: pre; + } &.plain-readme { background: none; @@ -303,11 +300,10 @@ body { } .page-title-empty { - margin-top: 0; + margin: 12px 0; line-height: 1.3; font-size: 1.25em; font-weight: $gl-font-weight-bold; - margin: 12px 0; } h1, diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 96dab609a13..dc1a73ed923 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -251,7 +251,7 @@ $gl-padding-top: 10px; $gl-sidebar-padding: 22px; $gl-bar-padding: 3px; $input-horizontal-padding: 12px; -$browserScrollbarSize: 10px; +$browser-scrollbar-size: 10px; /* * Misc @@ -405,6 +405,8 @@ $status-icon-size: 22px; $award-emoji-menu-shadow: rgba(0, 0, 0, 0.175); $award-emoji-positive-add-bg: #fed159; $award-emoji-positive-add-lines: #bb9c13; +$award-emoji-width: 376px; +$award-emoji-width-xs: 300px; /* * Search Box diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 604f806dc58..ca9a2a673f5 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -125,7 +125,7 @@ $dark-il: #de935f; .diff-line-num.new, .line_content.new { - @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border); + @include diff-background($dark-new-bg, $dark-new-idiff, $dark-border); &::before, a { @@ -135,7 +135,7 @@ $dark-il: #de935f; .diff-line-num.old, .line_content.old { - @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border); + @include diff-background($dark-old-bg, $dark-old-idiff, $dark-border); &::before, a { diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 8e2720511da..bc3761d1e47 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -125,7 +125,7 @@ $monokai-gi: #a6e22e; .diff-line-num.new, .line_content.new { - @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border); + @include diff-background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border); &::before, a { @@ -135,7 +135,7 @@ $monokai-gi: #a6e22e; .diff-line-num.old, .line_content.old { - @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border); + @include diff-background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border); &::before, a { diff --git a/app/assets/stylesheets/highlight/none.scss b/app/assets/stylesheets/highlight/none.scss index 7ced4e82e66..4bedb6a8e5b 100644 --- a/app/assets/stylesheets/highlight/none.scss +++ b/app/assets/stylesheets/highlight/none.scss @@ -4,7 +4,7 @@ -@mixin matchLine { +@mixin match-line { color: $black-transparent; background-color: $white-normal; } @@ -45,7 +45,7 @@ &.match .line_content, .new-nonewline.line_content, .old-nonewline.line_content { - @include matchLine; + @include match-line; } .diff-line-num { @@ -121,7 +121,7 @@ } &.match { - @include matchLine; + @include match-line; } &.hll:not(.empty-cell) { diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index cd1f0f6650f..de7b9424340 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -129,7 +129,7 @@ $solarized-dark-il: #2aa198; .diff-line-num.new, .line_content.new { - @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border); + @include diff-background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border); &::before, a { @@ -139,7 +139,7 @@ $solarized-dark-il: #2aa198; .diff-line-num.old, .line_content.old { - @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border); + @include diff-background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border); &::before, a { diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 09c3ea36414..84a92d0320a 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -90,7 +90,7 @@ $solarized-light-vg: #268bd2; $solarized-light-vi: #268bd2; $solarized-light-il: #2aa198; -@mixin matchLine { +@mixin match-line { color: $black-transparent; background: $solarized-light-matchline-bg; } @@ -125,7 +125,7 @@ $solarized-light-il: #2aa198; &.match .line_content, &.old-nonewline .line_content, &.new-nonewline .line_content { - @include matchLine; + @include match-line; } td.diff-line-num.hll:not(.empty-cell), @@ -136,7 +136,7 @@ $solarized-light-il: #2aa198; .diff-line-num.new, .line_content.new { - @include diff_background($solarized-light-new-bg, + @include diff-background($solarized-light-new-bg, $solarized-light-new-idiff, $solarized-light-border); &::before, @@ -147,7 +147,7 @@ $solarized-light-il: #2aa198; .diff-line-num.old, .line_content.old { - @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border); + @include diff-background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border); &::before, a { @@ -168,7 +168,7 @@ $solarized-light-il: #2aa198; } .line_content.match { - @include matchLine; + @include match-line; } &:not(.diff-expanded) + .diff-expanded, diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index 90a5250c247..c636abbdfad 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -70,7 +70,7 @@ $white-gc-color: #999; $white-gc-bg: #eaf2f5; -@mixin matchLine { +@mixin match-line { color: $black-transparent; background-color: $gray-light; } @@ -105,7 +105,7 @@ pre.code, &.match .line_content, .new-nonewline.line_content, .old-nonewline.line_content { - @include matchLine; + @include match-line; } .diff-line-num { @@ -185,7 +185,7 @@ pre.code, } &.match { - @include matchLine; + @include match-line; } &.hll:not(.empty-cell) { diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 2ac98b5d18f..a80158943c6 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -682,25 +682,6 @@ $ide-commit-header-height: 48px; flex: 1; } -.drag-handle { - position: absolute; - top: 0; - bottom: 0; - width: 4px; - - &:hover { - background-color: $white-normal; - } - - &.drag-right { - right: 0; - } - - &.drag-left { - left: 0; - } -} - .ide-commit-list-container { display: flex; flex: 1; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index bc28ffb3a92..a9324ba2ed0 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -164,6 +164,13 @@ display: none; } } + + &:not(.is-collapsed) { + .board-list-component { + display: flex; + flex-direction: column; + } + } } .board-inner { diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss index 38fec3f0aa8..ce0622b3d48 100644 --- a/app/assets/stylesheets/pages/branches.scss +++ b/app/assets/stylesheets/pages/branches.scss @@ -11,15 +11,24 @@ } .divergence-graph { + $graph-side-width: 80px; + $graph-separator-width: 1px; + padding: 0 6px; .graph-side { position: relative; - width: 80px; + width: $graph-side-width; height: 22px; padding: 5px 0 13px; float: left; + &.full { + width: $graph-side-width * 2 + $graph-separator-width; + display: flex; + justify-content: center; + } + .bar { position: absolute; height: 4px; @@ -57,7 +66,7 @@ .graph-separator { position: relative; - width: 1px; + width: $graph-separator-width; height: 18px; margin: 5px 0 0; float: left; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 37ed5ae674a..cb5f1a84005 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -34,7 +34,6 @@ .detail-page-header-actions { align-self: center; - flex-shrink: 0; flex: 0 0 auto; @include media-breakpoint-down(xs) { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index e3b98b26a11..ae0768592e0 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -602,7 +602,7 @@ } } -@mixin diff_background($background, $idiff, $border) { +@mixin diff-background($background, $idiff, $border) { background: $background; &.line_content span.idiff { @@ -1038,12 +1038,30 @@ } .diff-tree-list { - width: 320px; + position: -webkit-sticky; + position: sticky; + $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; + top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; + max-height: calc(100vh - #{$top-pos}); + padding-right: $gl-padding; + z-index: 202; + + .with-performance-bar & { + $performance-bar-top-pos: $performance-bar-height + $top-pos; + top: $performance-bar-top-pos; + max-height: calc(100vh - #{$performance-bar-top-pos}); + } + + .drag-handle { + bottom: 16px; + transform: translateX(-6px); + } } .diff-files-holder { flex: 1; min-width: 0; + z-index: 201; } .compare-versions-container { @@ -1051,23 +1069,12 @@ } .tree-list-holder { - position: -webkit-sticky; - position: sticky; - $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; - top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; - max-height: calc(100vh - #{$top-pos}); - padding-right: $gl-padding; + height: 100%; .file-row { margin-left: 0; margin-right: 0; } - - .with-performance-bar & { - $performance-bar-top-pos: $performance-bar-height + $top-pos; - top: $performance-bar-top-pos; - max-height: calc(100vh - #{$performance-bar-top-pos}); - } } .tree-list-scroll { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 5a988b184b6..655b297295a 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -182,9 +182,8 @@ .template-selector-dropdowns-wrap { display: inline-block; - margin-left: 8px; - vertical-align: top; margin: 5px 0 0 8px; + vertical-align: top; @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss index a4f76a9495a..7f800367cad 100644 --- a/app/assets/stylesheets/pages/import.scss +++ b/app/assets/stylesheets/pages/import.scss @@ -1,20 +1,51 @@ -.import-jobs-from-col, .import-jobs-to-col { - width: 40%; + width: 39%; } .import-jobs-status-col { - width: 20%; + width: 15%; } -.btn-import { - .loading-icon { - display: none; +.import-jobs-cta-col { + width: 1%; +} + +.import-project-name-input { + border-radius: 0 $border-radius-default $border-radius-default 0; + position: relative; + left: -1px; + max-width: 300px; +} + +.import-namespace-select { + width: auto !important; + + > .select2-choice { + border-radius: $border-radius-default 0 0 $border-radius-default; + position: relative; + left: 1px; } +} - &.is-loading { - .loading-icon { - display: inline-block; - } +.import-slash-divider { + background-color: $gray-lightest; + border: 1px solid $border-color; +} + +.import-row { + height: 55px; +} + +.import-table { + .import-jobs-from-col, + .import-jobs-to-col, + .import-jobs-status-col, + .import-jobs-cta-col { + border-bottom-width: 1px; + padding-left: $gl-padding; } } + +.import-projects-loading-icon { + margin-top: $gl-padding-32; +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 135730d71e9..883c856870f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -735,9 +735,11 @@ .mr-version-controls { position: relative; - z-index: 103; + z-index: 203; background: $gray-light; color: $gl-text-color; + margin-top: -1px; + border-top: 1px solid $border-color; .mr-version-menus-container { display: flex; @@ -789,7 +791,6 @@ position: sticky; top: $header-height + $mr-tabs-height; width: 100%; - border-top: 1px solid $border-color; &.is-fileTreeOpen { margin-left: -16px; @@ -808,12 +809,9 @@ .merge-request-tabs-holder { top: $header-height; - z-index: 200; + z-index: 300; background-color: $white-light; - - @include media-breakpoint-down(md) { - border-bottom: 1px solid $border-color; - } + border-bottom: 1px solid $border-color; @include media-breakpoint-up(sm) { position: sticky; @@ -1019,3 +1017,8 @@ z-index: 99999; background: $black-transparent; } + +.source-branch-removal-status { + padding-left: 50px; + padding-bottom: $gl-padding; +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 23b9e4f9416..7e7eff1346a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -494,11 +494,6 @@ $note-form-margin-left: 72px; .discussion-notes { margin-left: 0; border-left: 0; - - .notes { - position: relative; - @include vertical-line(52px); - } } .note-wrapper { @@ -550,6 +545,11 @@ $note-form-margin-left: 72px; .note-header-info { padding-bottom: 0; } + + .timeline-content { + overflow-x: auto; + overflow-y: hidden; + } } .unresolved { @@ -597,7 +597,6 @@ $note-form-margin-left: 72px; .note-headline-meta { display: inline-block; - white-space: nowrap; .system-note-message { white-space: normal; @@ -607,6 +606,10 @@ $note-form-margin-left: 72px; color: $gl-text-color-disabled; } + .note-timestamp { + white-space: nowrap; + } + a:hover { text-decoration: underline; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 66866aedfba..277030ad3af 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -704,8 +704,8 @@ .scrolling-tabs-container { .scrolling-tabs { margin-top: $gl-padding-8; - margin-bottom: $gl-padding-8 - $browserScrollbarSize; - padding-bottom: $browserScrollbarSize; + margin-bottom: $gl-padding-8 - $browser-scrollbar-size; + padding-bottom: $browser-scrollbar-size; flex-wrap: wrap; border-bottom: 0; } @@ -713,7 +713,7 @@ .fade-left, .fade-right { top: 0; - height: calc(100% - #{$browserScrollbarSize}); + height: calc(100% - #{$browser-scrollbar-size}); .fa { top: 50%; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index e93be1c1ba2..0eae007715a 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Admin::UsersController < Admin::ApplicationController + include RoutableActions + before_action :user, except: [:index, :new, :create] before_action :check_impersonation_availability, only: :impersonate @@ -177,11 +179,13 @@ class Admin::UsersController < Admin::ApplicationController user == current_user end - # rubocop: disable CodeReuse/ActiveRecord def user - @user ||= User.find_by!(username: params[:id]) + @user ||= find_routable!(User, params[:id]) + end + + def build_canonical_path(user) + url_for(safe_params.merge(id: user.to_param)) end - # rubocop: enable CodeReuse/ActiveRecord def redirect_back_or_admin_user(options = {}) redirect_back_or_default(default: default_route, options: options) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 26cd5dc801f..af0b0c64814 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -137,6 +137,8 @@ class ApplicationController < ActionController::Base if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze payload[:response] = response.body end + + payload[:queue_duration] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY] end ## diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 9ca54c5519b..28e4cece548 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -3,7 +3,7 @@ module SendFileUpload def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment') if attachment - response_disposition = ::Gitlab::ContentDisposition.format(disposition: 'attachment', filename: attachment) + response_disposition = ::Gitlab::ContentDisposition.format(disposition: disposition, filename: attachment) # Response-Content-Type will not override an existing Content-Type in # Google Cloud Storage, so the metadata needs to be cleared on GCS for diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 9484e4d30cd..912036da0ea 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -25,8 +25,6 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController private def group_milestones - groups = GroupsFinder.new(current_user, all_available: false).execute - DashboardGroupMilestone.build_collection(groups, params) end @@ -45,6 +43,6 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController end def groups - @groups ||= GroupsFinder.new(current_user, state_all: true).execute + @groups ||= GroupsFinder.new(current_user, all_available: false).execute end end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index e5a1fc9d6ff..a9d6addd4a4 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -13,9 +13,10 @@ class HelpController < ApplicationController # Remove YAML frontmatter so that it doesn't look weird @help_index = File.read(Rails.root.join('doc', 'README.md')).sub(YAML_FRONT_MATTER_REGEXP, '') - # Prefix Markdown links with `help/` unless they are external links - # See http://rubular.com/r/X3baHTbPO2 - @help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do + # Prefix Markdown links with `help/` unless they are external links. + # '//' not necessarily part of URL, e.g., mailto:mail@example.com + # See https://rubular.com/r/DFHZl5w8d3bpzV + @help_index.gsub!(%r{(?<delim>\]\()(?!\w+:)(?!/)(?<link>[^\)\(]+\))}) do "#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}" end end diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index f067ef625aa..68ad8650dba 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class Import::GiteaController < Import::GithubController + extend ::Gitlab::Utils::Override + def new - if session[access_token_key].present? && session[host_key].present? + if session[access_token_key].present? && provider_url.present? redirect_to status_import_url end end @@ -12,8 +14,8 @@ class Import::GiteaController < Import::GithubController super end + # Must be defined or it will 404 def status - @gitea_host_url = session[host_key] super end @@ -23,25 +25,33 @@ class Import::GiteaController < Import::GithubController :"#{provider}_host_url" end - # Overridden methods + override :provider def provider :gitea end + override :provider_url + def provider_url + session[host_key] + end + # Gitea is not yet an OAuth provider # See https://github.com/go-gitea/gitea/issues/27 + override :logged_in_with_provider? def logged_in_with_provider? false end + override :provider_auth def provider_auth - if session[access_token_key].blank? || session[host_key].blank? + if session[access_token_key].blank? || provider_url.blank? redirect_to new_import_gitea_url, alert: 'You need to specify both an Access Token and a Host URL.' end end + override :client_options def client_options - { host: session[host_key], api_version: 'v1' } + { host: provider_url, api_version: 'v1' } end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 3fbc0817e95..aa4aa0fbdac 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true class Import::GithubController < Import::BaseController + include ImportHelper + before_action :verify_import_enabled - before_action :provider_auth, only: [:status, :jobs, :create] + before_action :provider_auth, only: [:status, :realtime_changes, :create] + before_action :expire_etag_cache, only: [:status, :create] rescue_from Octokit::Unauthorized, with: :provider_unauthorized @@ -24,30 +27,37 @@ class Import::GithubController < Import::BaseController redirect_to status_import_url end - # rubocop: disable CodeReuse/ActiveRecord def status - @repos = client.repos - @already_added_projects = find_already_added_projects(provider) - already_added_projects_names = @already_added_projects.pluck(:import_source) - - @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } - end - # rubocop: enable CodeReuse/ActiveRecord - - def jobs - render json: find_jobs(provider) + # Request repos to display error page if provider token is invalid + # Improving in https://gitlab.com/gitlab-org/gitlab-ce/issues/55585 + client_repos + + respond_to do |format| + format.json do + render json: { imported_projects: serialized_imported_projects, + provider_repos: serialized_provider_repos, + namespaces: serialized_namespaces } + end + format.html + end end def create result = Import::GithubService.new(client, current_user, import_params).execute(access_params, provider) if result[:status] == :success - render json: ProjectSerializer.new.represent(result[:project]) + render json: serialized_imported_projects(result[:project]) else render json: { errors: result[:message] }, status: result[:http_status] end end + def realtime_changes + Gitlab::PollingInterval.set_header(response, interval: 3_000) + + render json: find_jobs(provider) + end + private def import_params @@ -58,10 +68,45 @@ class Import::GithubController < Import::BaseController [:repo_id, :new_name, :target_namespace] end + def serialized_imported_projects(projects = already_added_projects) + ProjectSerializer.new.represent(projects, serializer: :import, provider_url: provider_url) + end + + def serialized_provider_repos + repos = client_repos.reject { |repo| already_added_project_names.include? repo.full_name } + ProviderRepoSerializer.new(current_user: current_user).represent(repos, provider: provider, provider_url: provider_url) + end + + def serialized_namespaces + NamespaceSerializer.new.represent(namespaces) + end + + def already_added_projects + @already_added_projects ||= find_already_added_projects(provider) + end + + def already_added_project_names + @already_added_projects_names ||= already_added_projects.pluck(:import_source) # rubocop:disable CodeReuse/ActiveRecord + end + + def namespaces + current_user.manageable_groups_with_routes + end + + def expire_etag_cache + Gitlab::EtagCaching::Store.new.tap do |store| + store.touch(realtime_changes_path) + end + end + def client @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) end + def client_repos + @client_repos ||= client.repos + end + def verify_import_enabled render_404 unless import_enabled? end @@ -74,6 +119,10 @@ class Import::GithubController < Import::BaseController __send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend end + def realtime_changes_path + public_send("realtime_changes_import_#{provider}_path", format: :json) # rubocop:disable GitlabSecurity/PublicSend + end + def new_import_url public_send("new_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end @@ -105,6 +154,14 @@ class Import::GithubController < Import::BaseController :github end + def provider_url + strong_memoize(:provider_url) do + provider = Gitlab::Auth::OAuth::Provider.config_for('github') + + provider&.dig('url').presence || 'https://github.com' + end + end + # rubocop: disable CodeReuse/ActiveRecord def logged_in_with_provider? current_user.identities.exists?(provider: provider) diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index a6bfb913900..32b7f3207ef 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -29,7 +29,8 @@ class Projects::BranchesController < Projects::ApplicationController Gitlab::GitalyClient.allow_n_plus_1_calls do @max_commits = @branches.reduce(0) do |memo, branch| diverging_commit_counts = repository.diverging_commit_counts(branch) - [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + [memo, diverging_commit_counts.values_at(:behind, :ahead, :distance)] + .flatten.compact.max end end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index ddffbb17ace..518d41bd3fb 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController - include DiffForPath include DiffHelper include RendersNotes diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index 439ec9b1731..58b1bc54181 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -4,7 +4,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController layout 'project_settings' before_action :require_pages_enabled! - before_action :authorize_update_pages!, except: [:show] + before_action :authorize_update_pages! before_action :domain, except: [:new, :create] def show diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb index 54f01c99d78..7d0cb777ad1 100644 --- a/app/graphql/mutations/merge_requests/base.rb +++ b/app/graphql/mutations/merge_requests/base.rb @@ -25,7 +25,8 @@ module Mutations def find_object(project_path:, iid:) project = resolve_project(full_path: project_path) - resolver = Resolvers::MergeRequestResolver.new(object: project, context: context) + resolver = Resolvers::MergeRequestsResolver + .single.new(object: project, context: context) resolver.resolve(iid: iid) end diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 459933af9d3..063def75d38 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -2,5 +2,12 @@ module Resolvers class BaseResolver < GraphQL::Schema::Resolver + def self.single + @single ||= Class.new(self) do + def resolve(**args) + super.first + end + end + end end end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 95e66fb3b7c..fd1b46ba860 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -2,7 +2,9 @@ module Resolvers class IssuesResolver < BaseResolver - extend ActiveSupport::Concern + argument :iid, GraphQL::ID_TYPE, + required: false, + description: 'The IID of the issue, e.g., "1"' argument :iids, [GraphQL::ID_TYPE], required: false, @@ -22,6 +24,7 @@ module Resolvers # Will need to be be made group & namespace aware with # https://gitlab.com/gitlab-org/gitlab-ce/issues/54520 args[:project_id] = project.id + args[:iids] ||= [args[:iid]].compact IssuesFinder.new(context[:current_user], args).execute end diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index d047ce9e3a1..90795c797ac 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -1,19 +1,30 @@ # frozen_string_literal: true module Resolvers - class MergeRequestResolver < BaseResolver + class MergeRequestsResolver < BaseResolver argument :iid, GraphQL::ID_TYPE, - required: true, - description: 'The IID of the merge request, e.g., "1"' + required: false, + description: 'The IID of the merge request, e.g., "1"' + + argument :iids, [GraphQL::ID_TYPE], + required: false, + description: 'The list of IIDs of issues, e.g., [1, 2]' type Types::MergeRequestType, null: true alias_method :project, :object - # rubocop: disable CodeReuse/ActiveRecord - def resolve(iid:) + def resolve(**args) return unless project.present? + args[:iids] ||= [args[:iid]].compact + + args[:iids].map { |iid| batch_load(iid) } + .select(&:itself) # .compact doesn't work on BatchLoader + end + + # rubocop: disable CodeReuse/ActiveRecord + def batch_load(iid) BatchLoader.for(iid.to_s).batch(key: project) do |iids, loader, args| args[:key].merge_requests.where(iid: iids).each do |mr| loader.call(mr.iid.to_s, mr) diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 47b915b451e..7e63d4022b1 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -38,7 +38,6 @@ module Types field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false - field :diff_head_sha, GraphQL::STRING_TYPE, null: true field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage" field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 050706f97be..d25c8c8bd90 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -66,10 +66,17 @@ module Types field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :merge_requests, + Types::MergeRequestType.connection_type, + null: true, + resolver: Resolvers::MergeRequestsResolver do + authorize :read_merge_request + end + field :merge_request, Types::MergeRequestType, null: true, - resolver: Resolvers::MergeRequestResolver do + resolver: Resolvers::MergeRequestsResolver.single do authorize :read_merge_request end @@ -78,6 +85,11 @@ module Types null: true, resolver: Resolvers::IssuesResolver + field :issue, + Types::IssueType, + null: true, + resolver: Resolvers::IssuesResolver.single + field :pipelines, Types::Ci::PipelineType.connection_type, null: false, diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 473c90c882c..7fbbbb04154 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -28,7 +28,7 @@ module AppearancesHelper def brand_header_logo if current_appearance&.header_logo? - image_tag current_appearance.header_logo_path + image_tag current_appearance.header_logo_path, class: 'brand-header-logo' else render 'shared/logo.svg' end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index d3befd87ccc..3d494c3de6a 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -18,10 +18,8 @@ module ImportHelper "#{namespace}/#{name}" end - def provider_project_link(provider, full_path) - url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend - - link_to full_path, url, target: '_blank', rel: 'noopener noreferrer' + def provider_project_link_url(provider_url, full_path) + Gitlab::Utils.append_path(provider_url, full_path) end def import_will_timeout_message(_ci_cd_only) @@ -46,10 +44,6 @@ module ImportHelper _('Please wait while we import the repository for you. Refresh at will.') end - def import_github_title - _('Import repositories from GitHub') - end - def import_github_authorize_message _('To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:') end @@ -73,30 +67,4 @@ module ImportHelper _('Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link } end end - - def import_githubish_choose_repository_message - _('Choose which repositories you want to import.') - end - - def import_all_githubish_repositories_button_label - _('Import all repositories') - end - - private - - def github_project_url(full_path) - Gitlab::Utils.append_path(github_root_url, full_path) - end - - def github_root_url - strong_memoize(:github_url) do - provider = Gitlab::Auth::OAuth::Provider.config_for('github') - - provider&.dig('url').presence || 'https://github.com' - end - end - - def gitea_project_url(full_path) - Gitlab::Utils.append_path(@gitea_host_url, full_path) - end end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 6c65e573307..ea3bcfc791a 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -5,11 +5,8 @@ module NamespacesHelper params.dig(:project, :namespace_id) || params[:namespace_id] end - # rubocop: disable CodeReuse/ActiveRecord def namespaces_options(selected = :current_user, display_path: false, groups: nil, extra_group: nil, groups_only: false) - groups ||= current_user.manageable_groups - .eager_load(:route) - .order('routes.path') + groups ||= current_user.manageable_groups_with_routes users = [current_user.namespace] selected_id = selected @@ -43,7 +40,6 @@ module NamespacesHelper grouped_options_for_select(options, selected_id) end - # rubocop: enable CodeReuse/ActiveRecord def namespace_icon(namespace, size = 40) if namespace.is_a?(Group) diff --git a/app/models/board.rb b/app/models/board.rb index a137863456c..758a71d6903 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -21,6 +21,10 @@ class Board < ActiveRecord::Base group_id.present? end + def project_board? + project_id.present? + end + def backlog_list lists.merge(List.backlog).take end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index acef5d2e643..f0ae516a2f8 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -315,7 +315,7 @@ module Ci def ordered_stages return legacy_stages unless complete? - if Feature.enabled?('ci_pipeline_persisted_stages') + if Feature.enabled?('ci_pipeline_persisted_stages', default_enabled: true) stages else legacy_stages @@ -687,9 +687,18 @@ module Ci end end + # Returns the modified paths. + # + # The returned value is + # * Array: List of modified paths that should be evaluated + # * nil: Modified path can not be evaluated def modified_paths strong_memoize(:modified_paths) do - push_details.modified_paths + if merge_request? + merge_request.modified_paths + elsif branch_updated? + push_details.modified_paths + end end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7025fc2cc02..be3e6a05e1e 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -50,7 +50,7 @@ module Clusters validates :name, cluster_name: true validates :cluster_type, presence: true - validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } validate :restrict_modification, on: :update validate :no_groups, unless: :group_type? @@ -99,7 +99,7 @@ module Clusters where('NOT EXISTS (?)', subquery) end - scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.installed) } + scope :with_knative_installed, -> { joins(:application_knative).merge(Clusters::Applications::Knative.available) } scope :preload_knative, -> { preload( diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 5c0164831bc..1273ed83abe 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -6,7 +6,14 @@ module Clusters extend ActiveSupport::Concern included do - scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) } + scope :available, -> do + where( + status: [ + self.state_machines[:status].states[:installed].value, + self.state_machines[:status].states[:updated].value + ] + ) + end state_machine :status, initial: :not_installable do state :not_installable, value: -2 diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb index 42ec5b5e664..a9a2e9c81eb 100644 --- a/app/models/commit_collection.rb +++ b/app/models/commit_collection.rb @@ -20,8 +20,8 @@ class CommitCollection commits.each(&block) end - def committers - emails = without_merge_commits.map(&:committer_email).uniq + def authors + emails = without_merge_commits.map(&:author_email).uniq User.by_any_email(emails) end diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index d3572875fb3..de77ca3e963 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -76,7 +76,7 @@ module ReactiveCaching begin data = Rails.cache.read(full_reactive_cache_key(*args)) - yield data if data.present? + yield data unless data.nil? rescue InvalidateReactiveCache refresh_reactive_cache!(*args) nil diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb index d79c0eae77e..6c6febd186c 100644 --- a/app/models/concerns/with_uploads.rb +++ b/app/models/concerns/with_uploads.rb @@ -27,40 +27,14 @@ module WithUploads included do has_many :uploads, as: :model - has_many :file_uploads, -> { where(uploader: FILE_UPLOADERS) }, class_name: 'Upload', as: :model + has_many :file_uploads, -> { where(uploader: FILE_UPLOADERS) }, + class_name: 'Upload', as: :model, + dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - # TODO: when feature flag is removed, we can use just dependent: destroy - # option on :file_uploads - before_destroy :remove_file_uploads - - use_fast_destroy :file_uploads, if: :fast_destroy_enabled? + use_fast_destroy :file_uploads end def retrieve_upload(_identifier, paths) uploads.find_by(path: paths) end - - private - - # mounted uploads are deleted in carrierwave's after_commit hook, - # but FileUploaders which are not mounted must be deleted explicitly and - # it can not be done in after_commit because FileUploader requires loads - # associated model on destroy (which is already deleted in after_commit) - def remove_file_uploads - fast_destroy_enabled? ? delete_uploads : destroy_uploads - end - - def delete_uploads - file_uploads.delete_all(:delete_all) - end - - def destroy_uploads - file_uploads.find_each do |upload| - upload.destroy - end - end - - def fast_destroy_enabled? - Feature.enabled?(:fast_destroy_uploads, self) - end end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index f2678e0597d..32529ebf71d 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -17,8 +17,6 @@ class Discussion :for_commit?, :for_merge_request?, - :save, - to: :first_note def project_id diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb index aab0ff93468..b4a661ae5b4 100644 --- a/app/models/individual_note_discussion.rb +++ b/app/models/individual_note_discussion.rb @@ -17,8 +17,12 @@ class IndividualNoteDiscussion < Discussion noteable.supports_replying_to_individual_notes? && Feature.enabled?(:reply_to_individual_notes) end - def convert_to_discussion! - first_note.becomes!(Discussion.note_class).to_discussion + def convert_to_discussion!(save: false) + first_note.becomes!(Discussion.note_class).to_discussion.tap do + # Save needs to be called on first_note instead of the transformed note + # because of https://gitlab.com/gitlab-org/gitlab-ce/issues/57324 + first_note.save if save + end end def reply_attributes diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2035bffd829..75fca96ce0a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -286,12 +286,12 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end - def committers - @committers ||= commits.committers + def commit_authors + @commit_authors ||= commits.authors end def authors - User.from_union([committers, User.where(id: self.author_id)]) + User.from_union([commit_authors, User.where(id: self.author_id)]) end # Verifies if title has changed not taking into account WIP prefix @@ -1322,7 +1322,7 @@ class MergeRequest < ActiveRecord::Base def base_pipeline @base_pipeline ||= project.ci_pipelines .order(id: :desc) - .find_by(sha: diff_base_sha) + .find_by(sha: diff_base_sha, ref: target_branch) end def discussions_rendered_on_frontend? diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 712347e76ed..e286a4e57f2 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -12,9 +12,6 @@ class MergeRequestDiff < ActiveRecord::Base # Don't display more than 100 commits at once COMMITS_SAFE_SIZE = 100 - ignore_column :st_commits, - :st_diffs - belongs_to :merge_request manual_inverse_association :merge_request, :merge_request_diff diff --git a/app/models/project.rb b/app/models/project.rb index 58df4019450..c72d3a3b725 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2073,6 +2073,10 @@ class Project < ActiveRecord::Base pool_repository&.link_repository(repository) end + def has_pool_repository? + pool_repository.present? + end + private def merge_requests_allowing_collaboration(source_branch = nil) diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb index 6f639e5a7b2..6c507c47752 100644 --- a/app/models/releases/link.rb +++ b/app/models/releases/link.rb @@ -6,7 +6,7 @@ module Releases belongs_to :release - validates :url, presence: true, url: true, uniqueness: { scope: :release } + validates :url, presence: true, url: { protocols: %w(http https ftp) }, uniqueness: { scope: :release } validates :name, presence: true, uniqueness: { scope: :release } scope :sorted, -> { order(created_at: :desc) } diff --git a/app/models/repository.rb b/app/models/repository.rb index 7c50b4488e5..ed55a6e572b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -288,13 +288,16 @@ class Repository # Rugged seems to throw a `ReferenceError` when given branch_names rather # than SHA-1 hashes number_commits_behind, number_commits_ahead = - raw_repository.count_commits_between( + raw_repository.diverging_commit_count( @root_ref_hash, branch.dereferenced_target.sha, - left_right: true, max_count: MAX_DIVERGING_COUNT) - { behind: number_commits_behind, ahead: number_commits_ahead } + if number_commits_behind + number_commits_ahead >= MAX_DIVERGING_COUNT + { distance: MAX_DIVERGING_COUNT } + else + { behind: number_commits_behind, ahead: number_commits_ahead } + end end end diff --git a/app/models/user.rb b/app/models/user.rb index 24101eda0b1..fd32d838e53 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -275,6 +275,7 @@ class User < ApplicationRecord scope :confirmed, -> { where.not(confirmed_at: nil) } scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) } scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) } + scope :with_emails, -> { preload(:emails) } # Limits the users to those that have TODOs, optionally in the given state. # @@ -1167,6 +1168,10 @@ class User < ApplicationRecord Gitlab::ObjectHierarchy.new(owned_or_maintainers_groups).base_and_descendants end + def manageable_groups_with_routes + manageable_groups.eager_load(:route).order('routes.path') + end + def namespaces namespace_ids = groups.pluck(:id) namespace_ids.push(namespace.id) diff --git a/app/policies/board_policy.rb b/app/policies/board_policy.rb index 46db008421f..4bf1e7bd3e1 100644 --- a/app/policies/board_policy.rb +++ b/app/policies/board_policy.rb @@ -4,10 +4,12 @@ class BoardPolicy < BasePolicy delegate { @subject.parent } condition(:is_group_board) { @subject.group_board? } + condition(:is_project_board) { @subject.project_board? } - rule { is_group_board ? can?(:read_group) : can?(:read_project) }.enable :read_parent + rule { is_project_board & can?(:read_project) }.enable :read_parent rule { is_group_board & can?(:read_group) }.policy do + enable :read_parent enable :read_milestone enable :read_issue end diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index ea1d941cf83..4cac90c2567 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -256,7 +256,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated elsif repository.contribution_guide.present? AnchorData.new(false, statistic_icon('doc-text') + _('CONTRIBUTING'), - contribution_guide_path) + contribution_guide_path, + 'default') end end diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb index 06a8db78476..ede9e04b722 100644 --- a/app/serializers/diff_file_base_entity.rb +++ b/app/serializers/diff_file_base_entity.rb @@ -72,17 +72,20 @@ class DiffFileBaseEntity < Grape::Entity expose :old_path expose :new_path expose :new_file?, as: :new_file - expose :collapsed?, as: :collapsed - expose :text?, as: :text + expose :renamed_file?, as: :renamed_file + expose :deleted_file?, as: :deleted_file + expose :diff_refs + expose :stored_externally?, as: :stored_externally expose :external_storage - expose :renamed_file?, as: :renamed_file - expose :deleted_file?, as: :deleted_file + expose :mode_changed?, as: :mode_changed expose :a_mode expose :b_mode + expose :viewer, using: DiffViewerEntity + private def memoized_submodule_links(diff_file) diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index b0aaec3326d..01ee7af37ed 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -4,12 +4,10 @@ class DiffFileEntity < DiffFileBaseEntity include CommitsHelper include IconsHelper - expose :too_large?, as: :too_large - expose :empty?, as: :empty expose :added_lines expose :removed_lines - expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.text? && options[:merge_request] } do |diff_file| + expose :load_collapsed_diff_url, if: -> (diff_file, options) { diff_file.viewer.collapsed? && options[:merge_request] } do |diff_file| merge_request = options[:merge_request] project = merge_request.target_project @@ -36,10 +34,6 @@ class DiffFileEntity < DiffFileBaseEntity project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path)) end - expose :viewer, using: DiffViewerEntity do |diff_file| - diff_file.rich_viewer || diff_file.simple_viewer - end - expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file| image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image' image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha diff --git a/app/serializers/diff_viewer_entity.rb b/app/serializers/diff_viewer_entity.rb index 587fa2347fd..45faca6cb2f 100644 --- a/app/serializers/diff_viewer_entity.rb +++ b/app/serializers/diff_viewer_entity.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true class DiffViewerEntity < Grape::Entity - # Partial name refers directly to a Rails feature, let's avoid - # using this on the frontend. expose :partial_name, as: :name - expose :error do |diff_viewer| - diff_viewer.render_error_message - end + expose :render_error, as: :error + expose :render_error_message, as: :error_message + expose :collapsed?, as: :collapsed end diff --git a/app/serializers/namespace_basic_entity.rb b/app/serializers/namespace_basic_entity.rb new file mode 100644 index 00000000000..8bcbb2bca60 --- /dev/null +++ b/app/serializers/namespace_basic_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class NamespaceBasicEntity < Grape::Entity + expose :id + expose :full_path +end diff --git a/app/serializers/namespace_serializer.rb b/app/serializers/namespace_serializer.rb new file mode 100644 index 00000000000..bf3f154b558 --- /dev/null +++ b/app/serializers/namespace_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class NamespaceSerializer < BaseSerializer + entity NamespaceBasicEntity +end diff --git a/app/serializers/project_import_entity.rb b/app/serializers/project_import_entity.rb new file mode 100644 index 00000000000..9b51af685e7 --- /dev/null +++ b/app/serializers/project_import_entity.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ProjectImportEntity < ProjectEntity + include ImportHelper + + expose :import_source + expose :import_status + expose :human_import_status_name + + expose :provider_link do |project, options| + provider_project_link_url(options[:provider_url], project[:import_source]) + end +end diff --git a/app/serializers/project_serializer.rb b/app/serializers/project_serializer.rb index 23b96c2fc9e..52ac2fa0e09 100644 --- a/app/serializers/project_serializer.rb +++ b/app/serializers/project_serializer.rb @@ -1,5 +1,15 @@ # frozen_string_literal: true class ProjectSerializer < BaseSerializer - entity ProjectEntity + def represent(project, opts = {}) + entity = + case opts[:serializer] + when :import + ProjectImportEntity + else + ProjectEntity + end + + super(project, opts, entity) + end end diff --git a/app/serializers/provider_repo_entity.rb b/app/serializers/provider_repo_entity.rb new file mode 100644 index 00000000000..d70aaa91324 --- /dev/null +++ b/app/serializers/provider_repo_entity.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class ProviderRepoEntity < Grape::Entity + include ImportHelper + + expose :id + expose :full_name + expose :owner_name do |provider_repo, options| + owner_name(provider_repo, options[:provider]) + end + + expose :sanitized_name do |provider_repo| + sanitize_project_name(provider_repo[:name]) + end + + expose :provider_link do |provider_repo, options| + provider_project_link_url(options[:provider_url], provider_repo[:full_name]) + end + + private + + def owner_name(provider_repo, provider) + provider_repo.dig(:owner, :login) if provider == :github + end +end diff --git a/app/serializers/provider_repo_serializer.rb b/app/serializers/provider_repo_serializer.rb new file mode 100644 index 00000000000..8a73f6fe6df --- /dev/null +++ b/app/serializers/provider_repo_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ProviderRepoSerializer < BaseSerializer + entity ProviderRepoEntity +end diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb index b6c30da4d3a..dff0d9696f8 100644 --- a/app/services/applications/create_service.rb +++ b/app/services/applications/create_service.rb @@ -2,16 +2,16 @@ module Applications class CreateService - # rubocop: disable CodeReuse/ActiveRecord + attr_reader :current_user, :params + def initialize(current_user, params) @current_user = current_user - @params = params.except(:ip_address) + @params = params.except(:ip_address) # rubocop: disable CodeReuse/ActiveRecord end - # rubocop: enable CodeReuse/ActiveRecord # EE would override and use `request` arg def execute(request) - Doorkeeper::Application.create(@params) + Doorkeeper::Application.create(params) end end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 699b3e8555e..354e53a367c 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -36,7 +36,7 @@ module Ci project: project, current_user: current_user, push_options: params[:push_options], - **extra_options(**options)) + **extra_options(options)) sequence = Gitlab::Ci::Pipeline::Chain::Sequence .new(pipeline, command, SEQUENCE) @@ -108,7 +108,12 @@ module Ci end # rubocop: enable CodeReuse/ActiveRecord - def extra_options + def extra_options(options = {}) + # In Ruby 2.4, even when options is empty, f(**options) doesn't work when f + # doesn't have any parameters. We reproduce the Ruby 2.5 behavior by + # checking explicitely that no arguments are given. + raise ArgumentError if options.any? + {} # overriden in EE end end diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb index 28879d2d67f..2cb73555d85 100644 --- a/app/services/concerns/exclusive_lease_guard.rb +++ b/app/services/concerns/exclusive_lease_guard.rb @@ -42,7 +42,7 @@ module ExclusiveLeaseGuard def lease_timeout raise NotImplementedError, - "#{self.class.name} does not implement #{__method__}" + "#{self.class.name} does not implement #{__method__}" end def lease_release? diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 65208b07e27..110e589e30d 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class CreateBranchService < BaseService - def execute(branch_name, ref) - create_master_branch if project.empty_repo? + def execute(branch_name, ref, create_master_if_empty: true) + create_master_branch if create_master_if_empty && project.empty_repo? result = ValidateNewBranchService.new(project, current_user) .execute(branch_name) diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb index 988215ffc78..99324638300 100644 --- a/app/services/emails/base_service.rb +++ b/app/services/emails/base_service.rb @@ -2,10 +2,11 @@ module Emails class BaseService - attr_reader :current_user + attr_reader :current_user, :params, :user def initialize(current_user, params = {}) - @current_user, @params = current_user, params.dup + @current_user = current_user + @params = params.dup @user = params.delete(:user) end end diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb index 56925a724fe..dc06a5caa40 100644 --- a/app/services/emails/create_service.rb +++ b/app/services/emails/create_service.rb @@ -3,12 +3,11 @@ module Emails class CreateService < ::Emails::BaseService def execute(extra_params = {}) - skip_confirmation = @params.delete(:skip_confirmation) + skip_confirmation = params.delete(:skip_confirmation) - email = @user.emails.create(@params.merge(extra_params)) - - email&.confirm if skip_confirmation && current_user.admin? - email + user.emails.create(params.merge(extra_params)).tap do |email| + email&.confirm if skip_confirmation && current_user.admin? + end end end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 9ecee7c6156..092fd64574d 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -140,7 +140,7 @@ class GitPushService < BaseService .perform_async(project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) EventCreateService.new.push(project, current_user, build_push_data) - Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push) + Ci::CreatePipelineService.new(project, current_user, build_push_data).execute(:push, pipeline_options) project.execute_hooks(build_push_data.dup, :push_hooks) project.execute_services(build_push_data.dup, :push_hooks) @@ -231,4 +231,10 @@ class GitPushService < BaseService def last_pushed_commits @last_pushed_commits ||= @push_commits.last(PROCESS_COMMIT_LIMIT) end + + private + + def pipeline_options + {} # to be overriden in EE + end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 03fcf614c64..6fef5b3ed1d 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -10,7 +10,7 @@ class GitTagPushService < BaseService @push_data = build_push_data EventCreateService.new.push(project, current_user, push_data) - Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push) + Ci::CreatePipelineService.new(project, current_user, push_data).execute(:push, pipeline_options) SystemHooksService.new.execute_hooks(build_system_push_data, :tag_push_hooks) project.execute_hooks(push_data.dup, :tag_push_hooks) @@ -59,4 +59,8 @@ class GitTagPushService < BaseService [], '') end + + def pipeline_options + {} # to be overriden in EE + end end diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb index 52b45f1b2ce..3fb2c2b3007 100644 --- a/app/services/issues/build_service.rb +++ b/app/services/issues/build_service.rb @@ -57,9 +57,11 @@ module Issues end def issue_params - @issue_params ||= issue_params_with_info_from_discussions.merge(whitelisted_issue_params) + @issue_params ||= build_issue_params end + private + def whitelisted_issue_params if can?(current_user, :admin_issue, project) params.slice(:title, :description, :milestone_id) @@ -67,5 +69,9 @@ module Issues params.slice(:title, :description) end end + + def build_issue_params + issue_params_with_info_from_discussions.merge(whitelisted_issue_params) + end end end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index b975c3a8cb6..5a6e7338b42 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -35,7 +35,7 @@ module Notes if !only_commands && note.save if note.part_of_discussion? && note.discussion.can_convert_to_discussion? - note.discussion.convert_to_discussion!.save(touch: false) + note.discussion.convert_to_discussion!(save: true) end todo_service.new_note(note, current_user) diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index 7ee9732040d..985a03060bd 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -7,9 +7,14 @@ module Notes 'MergeRequest' => MergeRequests::UpdateService, 'Commit' => Commits::TagService }.freeze + private_constant :UPDATE_SERVICES + + def self.update_services + UPDATE_SERVICES + end def self.noteable_update_service(note) - UPDATE_SERVICES[note.noteable_type] + update_services[note.noteable_type] end def self.supported?(note) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 68cdc69023a..56f11b31110 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -249,6 +249,7 @@ module NotificationRecipientService attr_reader :action attr_reader :previous_assignee attr_reader :skip_current_user + def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true) @target = target @current_user = current_user @@ -258,9 +259,13 @@ module NotificationRecipientService @skip_current_user = skip_current_user end + def add_watchers + add_project_watchers + end + def build! add_participants(current_user) - add_project_watchers + add_watchers add_custom_notifications # Re-assign is considered as a mention of the new assignee diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb index 9b85e13107b..1b13dace5f2 100644 --- a/app/services/protected_branches/api_service.rb +++ b/app/services/protected_branches/api_service.rb @@ -3,16 +3,15 @@ module ProtectedBranches class ApiService < BaseService def create - @push_params = AccessLevelParams.new(:push, params) - @merge_params = AccessLevelParams.new(:merge, params) + ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute + end - protected_branch_params = { + def protected_branch_params + { name: params[:name], - push_access_levels_attributes: @push_params.access_levels, - merge_access_levels_attributes: @merge_params.access_levels + push_access_levels_attributes: AccessLevelParams.new(:push, params).access_levels, + merge_access_levels_attributes: AccessLevelParams.new(:merge, params).access_levels } - - ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute end end end diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb index da8bf2ce02a..7cb8d41818f 100644 --- a/app/services/protected_branches/legacy_api_update_service.rb +++ b/app/services/protected_branches/legacy_api_update_service.rb @@ -6,30 +6,31 @@ # lives in this service. module ProtectedBranches class LegacyApiUpdateService < BaseService + attr_reader :protected_branch, :developers_can_push, :developers_can_merge + def execute(protected_branch) + @protected_branch = protected_branch @developers_can_push = params.delete(:developers_can_push) @developers_can_merge = params.delete(:developers_can_merge) - @protected_branch = protected_branch - protected_branch.transaction do delete_redundant_access_levels - case @developers_can_push + case developers_can_push when true params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }] when false params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }] end - case @developers_can_merge + case developers_can_merge when true params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }] when false params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }] end - service = ProtectedBranches::UpdateService.new(@project, @current_user, @params) + service = ProtectedBranches::UpdateService.new(project, current_user, params) service.execute(protected_branch) end end @@ -37,12 +38,12 @@ module ProtectedBranches private def delete_redundant_access_levels - unless @developers_can_merge.nil? - @protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll + unless developers_can_merge.nil? + protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll end - unless @developers_can_push.nil? - @protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll + unless developers_can_push.nil? + protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll end end end diff --git a/app/services/task_list_toggle_service.rb b/app/services/task_list_toggle_service.rb index cfe187d9b12..f6602a35033 100644 --- a/app/services/task_list_toggle_service.rb +++ b/app/services/task_list_toggle_service.rb @@ -33,7 +33,7 @@ class TaskListToggleService markdown_task = source_lines[source_line_index] # The source in the DB could be using either \n or \r\n line endings - return unless markdown_task == line_source || markdown_task == line_source + "\r" + return unless markdown_task.chomp == line_source return unless source_checkbox = Taskable::ITEM_PATTERN.match(markdown_task) currently_checked = TaskList::Item.new(source_checkbox[1]).complete? @@ -67,6 +67,6 @@ class TaskListToggleService # When using CommonMark, we should be able to use the embedded `sourcepos` attribute to # target the exact line in the DOM. def get_html_checkbox(html) - html.css(".task-list-item[data-sourcepos^='#{line_number}:'] > input.task-list-item-checkbox").first + html.css(".task-list-item[data-sourcepos^='#{line_number}:'] input.task-list-item-checkbox").first end end diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb index db03ba8756f..e50840a9158 100644 --- a/app/services/users/activity_service.rb +++ b/app/services/users/activity_service.rb @@ -26,12 +26,15 @@ module Users def record_activity return if Gitlab::Database.read_only? + today = Date.today + + return if @user.last_activity_on == today + lease = Gitlab::ExclusiveLease.new("acitvity_service:#{@user.id}", timeout: LEASE_TIMEOUT) return unless lease.try_obtain - @user.update_attribute(:last_activity_on, Date.today) - Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@user.id} (username: #{@user.username})") + @user.update_attribute(:last_activity_on, today) end end end diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 004a3528d4b..9c7ca6ebbd4 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -3,12 +3,12 @@ = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = devise_error_messages! - .form-group + .name.form-group = f.label :name, 'Full name', class: 'label-bold' - = f.text_field :name, class: "form-control top qa-new-user-name", required: true, title: "This field is required." + = f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji", required: true, title: _("This field is required.") .username.form-group = f.label :username, class: 'label-bold' - = f.text_field :username, class: "form-control middle qa-new-user-username", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.' + = f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.") %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index f4a29ed18dc..b05c039c85c 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -1,56 +1,9 @@ - provider = local_assigns.fetch(:provider) - provider_title = Gitlab::ImportSources.title(provider) -%p.light - = import_githubish_choose_repository_message -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - = import_all_githubish_repositories_button_label - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _('From %{provider_title}') % { provider_title: provider_title } - %th= _('To GitLab') - %th= _('Status') - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: project_status_css_class(project.import_status) } - %td - = provider_project_link(provider, project.import_source) - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - = render 'import/project_status', project: project - - - @repos.each do |repo| - %tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } } - %td - = provider_project_link(provider, repo.full_name) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-prepend - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace qa-project-namespace-select', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true - %span.input-group-prepend - .input-group-text / - = text_field_tag :path, sanitize_project_name(repo.name), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - = has_ci_cd_only_params? ? _('Connect') : _('Import') - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: url_for([:jobs, :import, provider]), - import_path: url_for([:import, provider]), - ci_cd_only: has_ci_cd_only_params?.to_s } } +#import-projects-mount-element{ data: { provider: provider, provider_title: provider_title, + can_select_namespace: current_user.can_select_namespace?.to_s, + ci_cd_only: has_ci_cd_only_params?.to_s, + repos_path: url_for([:status, :import, provider, format: :json]), + jobs_path: url_for([:realtime_changes, :import, provider, format: :json]), + import_path: url_for([:import, provider, format: :json]) } } diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index 6ff25f2c842..cf32c5c9387 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -4,7 +4,7 @@ - header_title _("Projects"), root_path %h3.page-title - = icon 'github', text: import_github_title + = icon 'github', text: _('Import repositories from GitHub') - if github_import_configured? %p diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index be057be6d1a..ee295e70cce 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -2,7 +2,7 @@ - page_title title - breadcrumb_title title - header_title _("Projects"), root_path -%h3.page-title - = icon 'github', text: import_github_title +%h3.page-title.mb-0 + = icon 'github', class: 'fa-2x', text: _('Import repositories from GitHub') = render 'import/githubish_status', provider: 'github' diff --git a/app/views/import/manifest/status.html.haml b/app/views/import/manifest/status.html.haml index 5b2e1005398..3d4abc32b88 100644 --- a/app/views/import/manifest/status.html.haml +++ b/app/views/import/manifest/status.html.haml @@ -7,7 +7,7 @@ %p = button_tag class: "btn btn-import btn-success js-import-all" do - = import_all_githubish_repositories_button_label + = _('Import all repositories') = icon("spinner spin", class: "loading-icon") .table-responsive diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml index ddc1cdb24b5..26fd34347ec 100644 --- a/app/views/layouts/_mailer.html.haml +++ b/app/views/layouts/_mailer.html.haml @@ -49,7 +49,7 @@ %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" } %tbody %tr.line - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }  + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" } %tr.header %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } = header_logo diff --git a/app/views/layouts/header/_empty.html.haml b/app/views/layouts/header/_empty.html.haml index 2dfc787b7a8..348ce18b122 100644 --- a/app/views/layouts/header/_empty.html.haml +++ b/app/views/layouts/header/_empty.html.haml @@ -1,4 +1,2 @@ %header.navbar.fixed-top.navbar-empty - .container - .mx-auto - = brand_header_logo + = brand_header_logo diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 4b0ea15335e..c64ad1c8147 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -2,6 +2,7 @@ - commit = @repository.commit(branch.dereferenced_target) - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - diverging_commit_counts = @repository.diverging_commit_counts(branch) +- number_commits_distance = diverging_commit_counts[:distance] - number_commits_behind = diverging_commit_counts[:behind] - number_commits_ahead = diverging_commit_counts[:ahead] - merge_project = merge_request_source_project_for_project(@project) @@ -28,16 +29,23 @@ = s_('Branches|Cant find HEAD commit for this branch') - if branch.name != @repository.root_ref - .divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), - default_branch: @repository.root_ref, - number_commits_ahead: diverging_count_label(number_commits_ahead) } } - .graph-side - .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } - %span.count.count-behind= diverging_count_label(number_commits_behind) - .graph-separator - .graph-side - .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } - %span.count.count-ahead= diverging_count_label(number_commits_ahead) + - if number_commits_distance.nil? + .divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), + default_branch: @repository.root_ref, + number_commits_ahead: diverging_count_label(number_commits_ahead) } } + .graph-side + .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } + %span.count.count-behind= diverging_count_label(number_commits_behind) + .graph-separator + .graph-side + .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } + %span.count.count-ahead= diverging_count_label(number_commits_ahead) + - else + .divergence-graph.d-none.d-md-block{ title: s_('More than %{number_commits_distance} commits different with %{default_branch}') % { number_commits_distance: diverging_count_label(number_commits_distance), + default_branch: @repository.root_ref} } + .graph-side.full + .bar{ style: "width: #{number_commits_distance * bar_graph_width_factor}%" } + %span.count= diverging_count_label(number_commits_distance) .controls.d-none.d-md-block< - if merge_project && create_mr_button?(@repository.root_ref, branch.name) diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index eb677cff5f0..ae9aef5a9b0 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,2 +1,2 @@ - if commit.has_signature? - %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } + %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } } diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index 8b6e3e42ea1..41f5fb3dcbd 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -3,10 +3,10 @@ %ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs = nav_link(path: 'commit#show') do = link_to project_commit_path(@project, @commit.id) do - Changes + = _('Changes') %span.badge.badge-pill= @diffs.size - if any_pipelines = nav_link(path: 'commit#pipelines') do = link_to pipelines_project_commit_path(@project, @commit.id) do - Pipelines + = _('Pipelines') %span.badge.badge-pill.js-pipelines-mr-count= @commit.pipelines.size diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 90fee2d70be..a0db48bf8ff 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -6,8 +6,8 @@ %strong #{ s_('CommitBoxTitle|Commit') } %span.commit-sha= @commit.short_id - = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard")) - %span.d-none.d-sm-inline authored + = clipboard_button(text: @commit.id, title: _('Copy commit SHA to clipboard')) + %span.d-none.d-sm-inline= _('authored') #{time_ago_with_tooltip(@commit.authored_date)} %span= s_('ByAuthor|by') = author_avatar(@commit, size: 24, has_tooltip: false) @@ -43,13 +43,13 @@ = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) - if can?(current_user, :push_code, @project) %li.clearfix - = link_to s_("CreateTag|Tag"), new_project_tag_path(@project, ref: @commit) + = link_to s_('CreateTag|Tag'), new_project_tag_path(@project, ref: @commit) %li.divider %li.dropdown-header #{ _('Download') } - unless @commit.parents.length > 1 - %li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch), class: "qa-email-patches" - %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff" + %li= link_to s_('DownloadCommit|Email Patches'), project_commit_path(@project, @commit, format: :patch), class: "qa-email-patches" + %li= link_to s_('DownloadCommit|Plain Diff'), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff" .commit-box{ data: { project_path: project_path(@project) } } %h3.commit-title @@ -95,8 +95,5 @@ .well-segment = icon('info-circle fw') - This commit is part of merge request - = succeed '.' do - = link_to @merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id) - - Comments created here will be created in the context of that merge request. + - link_to_merge_request = link_to(@merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id)) + = _('This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request.').html_safe % { link_to_merge_request: link_to_merge_request } diff --git a/app/views/projects/commit/_limit_exceeded_message.html.haml b/app/views/projects/commit/_limit_exceeded_message.html.haml index a264f3517c4..7d3c0582d0b 100644 --- a/app/views/projects/commit/_limit_exceeded_message.html.haml +++ b/app/views/projects/commit/_limit_exceeded_message.html.haml @@ -1,8 +1,8 @@ -.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: "Project has too many #{label_for_message} to search"} } +.has-tooltip{ class: "limit-box limit-box-#{objects} prepend-left-5", data: { title: _('Project has too many %{label_for_message} to search') % { label_for_message: label_for_message } } } .limit-icon - if objects == :branch = sprite_icon('fork', size: 12) - else = icon('tag') .limit-message - %span #{label_for_message.capitalize} unavailable + %span= _('%{label_for_message} unavailable') % { label_for_message: label_for_message.capitalize } diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml index d7bf2dc0cb6..bb843bee7c9 100644 --- a/app/views/projects/commit/_other_user_signature_badge.html.haml +++ b/app/views/projects/commit/_other_user_signature_badge.html.haml @@ -1,6 +1,6 @@ - title = capture do - This commit was signed with a different user's verified signature. + = _("This commit was signed with a different user's verified signature.") -- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true } +- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml index 22ffd66ff8e..d282ab4f520 100644 --- a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml +++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml @@ -1,7 +1,6 @@ - title = capture do - This commit was signed with a verified signature, but the committer email - is <strong>not verified</strong> to belong to the same user. + = _('This commit was signed with a verified signature, but the committer email is <strong>not verified</strong> to belong to the same user.').html_safe -- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true } +- locals = { signature: signature, title: title, label: _('Unverified'), css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index c4d986ef742..1331fa179fc 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -19,10 +19,10 @@ .clearfix = render partial: 'projects/commit/signature_badge_user', locals: { signature: signature } - GPG Key ID: + = _('GPG Key ID:') %span.monospace= signature.gpg_key_primary_keyid - = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') + = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') %a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } = label diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml index 00e1efe0582..294f916d18f 100644 --- a/app/views/projects/commit/_unverified_signature_badge.html.haml +++ b/app/views/projects/commit/_unverified_signature_badge.html.haml @@ -1,6 +1,6 @@ - title = capture do - This commit was signed with an <strong>unverified</strong> signature. + = _('This commit was signed with an <strong>unverified</strong> signature.').html_safe -- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless' } +- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless' } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml index 31408806be7..4964b1b8ee7 100644 --- a/app/views/projects/commit/_verified_signature_badge.html.haml +++ b/app/views/projects/commit/_verified_signature_badge.html.haml @@ -1,7 +1,6 @@ - title = capture do - This commit was signed with a <strong>verified</strong> signature and the - committer email is verified to belong to the same user. + = _('This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user.').html_safe -- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'status_success_borderless', show_user: true } +- locals = { signature: signature, title: title, label: _('Verified'), css_class: 'valid', icon: 'status_success_borderless', show_user: true } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index c66ea873dba..f8c27f4c026 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -1,4 +1,4 @@ -- page_title 'Pipelines', "#{@commit.title} (#{@commit.short_id})", 'Commits' +- page_title _('Pipelines'), "#{@commit.title} (#{@commit.short_id})", _('Commits') = render 'commit_box' = render 'ci_menu' diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index fe9a8ac4182..34226167288 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,10 +1,10 @@ - @no_container = true -- add_to_breadcrumbs "Commits", project_commits_path(@project) +- add_to_breadcrumbs _('Commits'), project_commits_path(@project) - breadcrumb_title @commit.short_id - container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : '' - limited_container_width = fluid_layout ? '' : 'limit-container-width' - @content_class = limited_container_width -- page_title "#{@commit.title} (#{@commit.short_id})", "Commits" +- page_title "#{@commit.title} (#{@commit.short_id})", _('Commits') - page_description @commit.description .container-fluid{ class: [limited_container_width, container_class] } diff --git a/app/views/projects/issues/_import_export.svg b/app/views/projects/issues/_import_export.svg deleted file mode 100644 index 53c35d12f57..00000000000 --- a/app/views/projects/issues/_import_export.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 238 111" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="4" width="82" rx="3" height="28" fill="#fff"/><path id="5" d="m68.926 12.09v-2.41c0-.665-.437-.888-.975-.507l-6.552 4.631c-.542.383-.539.998 0 1.379l6.552 4.631c.542.383.975.154.975-.507v-2.41h4.874c.668 0 1.2-.538 1.2-1.201v-2.406c0-.668-.537-1.201-1.2-1.201h-4.874" fill="#fc8a51"/><path id="6" d="m4 24h74v-20h-74v20m-4-21c0-1.655 1.338-2.996 2.991-2.996h76.02c1.652 0 2.991 1.35 2.991 2.996v22.01c0 1.655-1.338 2.996-2.991 2.996h-76.02c-1.652 0-2.991-1.35-2.991-2.996v-22.01"/><circle id="2" cx="16" cy="14" r="7"/><circle id="0" cx="16" cy="14" r="7"/><mask id="3" width="14" height="14" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="14" height="14" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask></defs><g fill="none" fill-rule="evenodd"><rect width="98" height="111" fill="#fff" rx="6"/><path fill="#e5e5e5" fill-rule="nonzero" d="m4 6.01v98.99c0 1.11.897 2.01 2 2.01h85.998c1.105 0 2-.897 2-2.01v-98.99c0-1.11-.897-2.01-2-2.01h-85.998c-1.105 0-2 .897-2 2.01m-4 0c0-3.318 2.685-6.01 6-6.01h85.998c3.314 0 6 2.689 6 6.01v98.99c0 3.318-2.685 6.01-6 6.01h-85.998c-3.314 0-6-2.689-6-6.01v-98.99"/><rect width="76" height="85" x="11" y="12" fill="#f9f9f9" rx="3"/><g transform="translate(37 59)"><use xlink:href="#4"/><path fill="#e5e5e5" fill-rule="nonzero" d="m4 24h74v-20h-74v20m-4-21c0-1.655 1.338-2.996 2.991-2.996h76.02c1.652 0 2.991 1.35 2.991 2.996v22.01c0 1.655-1.338 2.996-2.991 2.996h-76.02c-1.652 0-2.991-1.35-2.991-2.996v-22.01"/><use fill="#fff" stroke="#6b4fbb" stroke-width="8" mask="url(#1)" xlink:href="#0"/><use xlink:href="#5"/></g><g transform="translate(140)"><path fill="#fff" d="m0 4h94v103h-94z"/><path fill="#e5e5e5" fill-rule="nonzero" d="m0 74v30.993c0 3.318 2.687 6.01 6 6.01h85.998c3.316 0 6-2.69 6-6.01v-98.99c0-3.318-2.687-6.01-6-6.01h-85.998c-3.316 0-6 2.69-6 6.01v.993h4v-.993c0-1.11.896-2.01 2-2.01h85.998c1.105 0 2 .897 2 2.01v98.99c0 1.11-.896 2.01-2 2.01h-85.998c-1.105 0-2-.897-2-2.01v-30.993h-4"/><g fill="#f9f9f9"><rect width="82" height="28" x="8" y="12" rx="3"/><rect width="82" height="28" x="8" y="43" rx="3"/></g></g><g fill-rule="nonzero" transform="translate(148 73)"><use fill="#e5e5e5" xlink:href="#6"/><path fill="#6b4fbb" d="m17 17c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3m0 4c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7"/></g><g transform="translate(25 24)"><use xlink:href="#4"/><use fill="#e5e5e5" fill-rule="nonzero" xlink:href="#6"/><use fill="#fff" stroke="#6b4fbb" stroke-width="8" mask="url(#3)" xlink:href="#2"/><use xlink:href="#5"/></g><g transform="translate(107 10)"><use xlink:href="#4"/><use fill="#fc8a51" fill-opacity=".3" fill-rule="nonzero" xlink:href="#6"/><path fill="#6b4fbb" fill-rule="nonzero" d="m16 17c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3m0 4c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7" id="7"/><use xlink:href="#5"/></g><g transform="translate(128 41)"><use xlink:href="#4"/><use fill="#fc8a51" fill-opacity=".3" fill-rule="nonzero" xlink:href="#6"/><use xlink:href="#7"/><path fill="#fc8a51" d="m66.926 12.09v-2.41c0-.665-.437-.888-.975-.507l-6.552 4.631c-.542.383-.539.998 0 1.379l6.552 4.631c.542.383.975.154.975-.507v-2.41h4.874c.668 0 1.2-.538 1.2-1.201v-2.406c0-.668-.537-1.201-1.2-1.201h-4.874"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/projects/issues/import_csv/_modal.html.haml b/app/views/projects/issues/import_csv/_modal.html.haml index 5339c4325b9..86bc54786ad 100644 --- a/app/views/projects/issues/import_csv/_modal.html.haml +++ b/app/views/projects/issues/import_csv/_modal.html.haml @@ -5,8 +5,8 @@ .modal-header %h3 = _('Import issues') - .import-export-svg-container - = render 'projects/issues/import_export.svg' + .svg-content.import-export-svg-container + = image_tag 'illustrations/export-import.svg', alt: _('Import/Export illustration'), class: 'illustration' %a.close{ href: '#', 'data-dismiss' => 'modal' } × .modal-body .modal-text diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 26671a7b7d2..1277ea6c743 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -23,9 +23,6 @@ = s_("Wiki|Create Page") .nav-controls - - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do - = s_("Wiki|New page") - if @page.persisted? = link_to project_wiki_history_path(@project, @page), class: "btn" do = s_("Wiki|Page history") diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml index 555cb4f4af9..bba3475d244 100644 --- a/app/views/shared/empty_states/_priority_labels.html.haml +++ b/app/views/shared/empty_states/_priority_labels.html.haml @@ -1,4 +1,4 @@ .text-center - .svg-content + .svg-content.qa-label-svg = image_tag 'illustrations/priority_labels.svg' %p Star labels to start sorting by priority diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0520eda37a4..9596c1df20e 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -133,7 +133,7 @@ #js-confidential-entry-point -# haml-lint:disable InlineJavaScript - %script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe + %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe #js-lock-entry-point .js-sidebar-participants-entry-point diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb index 7c66ac046ea..9ec8bcca4f3 100644 --- a/app/workers/reactive_caching_worker.rb +++ b/app/workers/reactive_caching_worker.rb @@ -6,7 +6,7 @@ class ReactiveCachingWorker # rubocop: disable CodeReuse/ActiveRecord def perform(class_name, id, *args) klass = begin - Kernel.const_get(class_name) + class_name.constantize rescue NameError nil end diff --git a/bin/secpick b/bin/secpick index 8f956d300a7..d01304285b6 100755 --- a/bin/secpick +++ b/bin/secpick @@ -45,9 +45,7 @@ module Secpick def git_commands ["git fetch #{@options[:remote]} #{stable_branch}", - "git checkout #{stable_branch}", - "git pull #{@options[:remote]} #{stable_branch}", - "git checkout -B #{source_branch}", + "git checkout -B #{source_branch} #{@options[:remote]}/#{stable_branch}", "git cherry-pick #{@options[:sha]}", "git push #{@options[:remote]} #{source_branch}", "git checkout #{original_branch}"] @@ -55,10 +53,10 @@ module Secpick def gitlab_params { + issuable_template: 'Security Release', merge_request: { source_branch: source_branch, - target_branch: stable_branch, - description: '/label ~security' + target_branch: stable_branch } } end diff --git a/changelogs/README.md b/changelogs/README.md new file mode 100644 index 00000000000..c4113ccb863 --- /dev/null +++ b/changelogs/README.md @@ -0,0 +1,10 @@ +# Generating changelog entries + +To generate and validate your changelog entries: + +1. Run `bin/changelog` to generate. +1. Run `scripts/lint-changelog-yaml` to validate. + +See [development/changelog] documentation for detailed usage. + +[development/changelog]: https://docs.gitlab.com/ee/development/changelog.html diff --git a/changelogs/unreleased/24642-activity_service_optimization.yml b/changelogs/unreleased/24642-activity_service_optimization.yml new file mode 100644 index 00000000000..bdfa769959e --- /dev/null +++ b/changelogs/unreleased/24642-activity_service_optimization.yml @@ -0,0 +1,5 @@ +--- +title: Optimize Redis usage in User::ActivityService +merge_request: 25005 +author: +type: performance diff --git a/changelogs/unreleased/39676-wiki-api-problems-on-update-parameters-and-500-error.yml b/changelogs/unreleased/39676-wiki-api-problems-on-update-parameters-and-500-error.yml new file mode 100644 index 00000000000..1af49fb6a2c --- /dev/null +++ b/changelogs/unreleased/39676-wiki-api-problems-on-update-parameters-and-500-error.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Require only one parameter when updating a wiki' +merge_request: 25191 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/44740-api-to-verify-a-given-user-has-right-to-merge-a-given-mergerequest.yml b/changelogs/unreleased/44740-api-to-verify-a-given-user-has-right-to-merge-a-given-mergerequest.yml new file mode 100644 index 00000000000..1c739130fcc --- /dev/null +++ b/changelogs/unreleased/44740-api-to-verify-a-given-user-has-right-to-merge-a-given-mergerequest.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Expose if the current user can merge a MR' +merge_request: 25207 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/47150-update-sshkey.yml b/changelogs/unreleased/47150-update-sshkey.yml new file mode 100644 index 00000000000..342bdb1e2bc --- /dev/null +++ b/changelogs/unreleased/47150-update-sshkey.yml @@ -0,0 +1,5 @@ +--- +title: Fix validation of certain ed25519 keys +merge_request: 25115 +author: Merlijn B. W. Wajer +type: fixed diff --git a/changelogs/unreleased/49502-gpg-signature-api-endpoint.yml b/changelogs/unreleased/49502-gpg-signature-api-endpoint.yml new file mode 100644 index 00000000000..8393cb9d282 --- /dev/null +++ b/changelogs/unreleased/49502-gpg-signature-api-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoint to get a commit's GPG signature +merge_request: 25032 +author: +type: added diff --git a/changelogs/unreleased/50006-expose-textcolor-from-public-labels-api.yml b/changelogs/unreleased/50006-expose-textcolor-from-public-labels-api.yml new file mode 100644 index 00000000000..3c8b58f3001 --- /dev/null +++ b/changelogs/unreleased/50006-expose-textcolor-from-public-labels-api.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Expose text_color for project and group labels' +merge_request: 25172 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/50433-make-emoji-picker-bigger.yml b/changelogs/unreleased/50433-make-emoji-picker-bigger.yml new file mode 100644 index 00000000000..8fcf41df09d --- /dev/null +++ b/changelogs/unreleased/50433-make-emoji-picker-bigger.yml @@ -0,0 +1,5 @@ +--- +title: Make emoji picker bigger +merge_request: 25187 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/54725-fix-emoji-button-active-state.yml b/changelogs/unreleased/54725-fix-emoji-button-active-state.yml new file mode 100644 index 00000000000..4f0a436cc87 --- /dev/null +++ b/changelogs/unreleased/54725-fix-emoji-button-active-state.yml @@ -0,0 +1,5 @@ +--- +title: Fix hover and active state colors of award emoji button +merge_request: 25295 +author: +type: fixed diff --git a/changelogs/unreleased/54850-pages-domain-show-view-is-not-protected-by-access-control.yml b/changelogs/unreleased/54850-pages-domain-show-view-is-not-protected-by-access-control.yml new file mode 100644 index 00000000000..41761213d7b --- /dev/null +++ b/changelogs/unreleased/54850-pages-domain-show-view-is-not-protected-by-access-control.yml @@ -0,0 +1,5 @@ +--- +title: Require maintainer access to show pages domain settings +merge_request: 24926 +author: +type: fixed diff --git a/changelogs/unreleased/55209-tool-tip-hides-menu-item.yml b/changelogs/unreleased/55209-tool-tip-hides-menu-item.yml new file mode 100644 index 00000000000..44ea4141632 --- /dev/null +++ b/changelogs/unreleased/55209-tool-tip-hides-menu-item.yml @@ -0,0 +1,5 @@ +--- +title: Close More Actions tooltip when menu opens +merge_request: 24285 +author: +type: fixed diff --git a/changelogs/unreleased/55312-svg.yml b/changelogs/unreleased/55312-svg.yml new file mode 100644 index 00000000000..a6260aeaf2a --- /dev/null +++ b/changelogs/unreleased/55312-svg.yml @@ -0,0 +1,5 @@ +--- +title: Use export-import svgs from gitlab-svgs +merge_request: 24954 +author: +type: other diff --git a/changelogs/unreleased/55893-artifacts-download.yml b/changelogs/unreleased/55893-artifacts-download.yml new file mode 100644 index 00000000000..30c118b7094 --- /dev/null +++ b/changelogs/unreleased/55893-artifacts-download.yml @@ -0,0 +1,5 @@ +--- +title: Fixes not working dropdowns in pipelines page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/56237-api-truncated-commit-title.yml b/changelogs/unreleased/56237-api-truncated-commit-title.yml new file mode 100644 index 00000000000..1a48d0fda1b --- /dev/null +++ b/changelogs/unreleased/56237-api-truncated-commit-title.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Expose full commit title' +merge_request: 25189 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/56332-exclude-public-group-milestones-from-count.yml b/changelogs/unreleased/56332-exclude-public-group-milestones-from-count.yml new file mode 100644 index 00000000000..50ca9c94173 --- /dev/null +++ b/changelogs/unreleased/56332-exclude-public-group-milestones-from-count.yml @@ -0,0 +1,5 @@ +--- +title: Fix counts in milestones dashboard +merge_request: 25230 +author: +type: fixed diff --git a/changelogs/unreleased/56485-implement-graphql-mergerequestsresolver.yml b/changelogs/unreleased/56485-implement-graphql-mergerequestsresolver.yml new file mode 100644 index 00000000000..5362ac65038 --- /dev/null +++ b/changelogs/unreleased/56485-implement-graphql-mergerequestsresolver.yml @@ -0,0 +1,5 @@ +--- +title: Add field mergeRequests for project in GraphQL +merge_request: 24805 +author: +type: added diff --git a/changelogs/unreleased/56694-mark-group-level-labels-in-label-api-as-such.yml b/changelogs/unreleased/56694-mark-group-level-labels-in-label-api-as-such.yml new file mode 100644 index 00000000000..ae2d9e18e0b --- /dev/null +++ b/changelogs/unreleased/56694-mark-group-level-labels-in-label-api-as-such.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Indicate if label is a project label' +merge_request: 25219 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/56787-realtime-validation-for-user-fullname-and-username.yml b/changelogs/unreleased/56787-realtime-validation-for-user-fullname-and-username.yml new file mode 100644 index 00000000000..cc3a60479d3 --- /dev/null +++ b/changelogs/unreleased/56787-realtime-validation-for-user-fullname-and-username.yml @@ -0,0 +1,5 @@ +--- +title: Add realtime validation for user fullname and username on validation +merge_request: 25017 +author: Ehsan Abdulqader @EhsanZ +type: added diff --git a/changelogs/unreleased/56851-blank-values-in-reactive-cache.yml b/changelogs/unreleased/56851-blank-values-in-reactive-cache.yml new file mode 100644 index 00000000000..5b9253793be --- /dev/null +++ b/changelogs/unreleased/56851-blank-values-in-reactive-cache.yml @@ -0,0 +1,5 @@ +--- +title: Allow empty values such as [] to be stored in reactive cache +merge_request: 25283 +author: +type: fixed diff --git a/changelogs/unreleased/56937-edit-knative-domain-after-it-has-been-deployed.yml b/changelogs/unreleased/56937-edit-knative-domain-after-it-has-been-deployed.yml new file mode 100644 index 00000000000..11d93b34700 --- /dev/null +++ b/changelogs/unreleased/56937-edit-knative-domain-after-it-has-been-deployed.yml @@ -0,0 +1,5 @@ +--- +title: Fixes functions finder for upgraded Knative app +merge_request: 25067 +author: +type: fixed diff --git a/changelogs/unreleased/57101-api-docs-for-hangouts-chat-service-incorrect.yml b/changelogs/unreleased/57101-api-docs-for-hangouts-chat-service-incorrect.yml new file mode 100644 index 00000000000..2e0ae9c3732 --- /dev/null +++ b/changelogs/unreleased/57101-api-docs-for-hangouts-chat-service-incorrect.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Fix docs and parameters for hangouts-chat service' +merge_request: 25180 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/57160-merge-request-tabs-header-is-missing-bottom-border.yml b/changelogs/unreleased/57160-merge-request-tabs-header-is-missing-bottom-border.yml new file mode 100644 index 00000000000..3146d07db3d --- /dev/null +++ b/changelogs/unreleased/57160-merge-request-tabs-header-is-missing-bottom-border.yml @@ -0,0 +1,5 @@ +--- +title: Return bottom border on MR Tabs +merge_request: !25198 +author: +type: fixed diff --git a/changelogs/unreleased/57223-wiki-finder.yml b/changelogs/unreleased/57223-wiki-finder.yml new file mode 100644 index 00000000000..5ddf197568d --- /dev/null +++ b/changelogs/unreleased/57223-wiki-finder.yml @@ -0,0 +1,5 @@ +--- +title: Remove BATCH_SIZE from WikiFileFinder +merge_request: 24933 +author: +type: other diff --git a/changelogs/unreleased/57353-git-push-fails-on-large-lfs-files-where-the-push-take-a-long-time.yml b/changelogs/unreleased/57353-git-push-fails-on-large-lfs-files-where-the-push-take-a-long-time.yml new file mode 100644 index 00000000000..46f82afda62 --- /dev/null +++ b/changelogs/unreleased/57353-git-push-fails-on-large-lfs-files-where-the-push-take-a-long-time.yml @@ -0,0 +1,5 @@ +--- +title: Provide expires_in in LFS authentication payload +merge_request: 25082 +author: +type: fixed diff --git a/changelogs/unreleased/57410-api-create-release-link-with-ftp-address-return-400-bad-request.yml b/changelogs/unreleased/57410-api-create-release-link-with-ftp-address-return-400-bad-request.yml new file mode 100644 index 00000000000..6be6a2115b9 --- /dev/null +++ b/changelogs/unreleased/57410-api-create-release-link-with-ftp-address-return-400-bad-request.yml @@ -0,0 +1,5 @@ +--- +title: Add support for FTP assets for releases +merge_request: 25071 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/57544-web-ide-new-directory-dialog-shows-file-templates.yml b/changelogs/unreleased/57544-web-ide-new-directory-dialog-shows-file-templates.yml new file mode 100644 index 00000000000..9d9158ca4af --- /dev/null +++ b/changelogs/unreleased/57544-web-ide-new-directory-dialog-shows-file-templates.yml @@ -0,0 +1,5 @@ +--- +title: Do not show file templates when creating a new directory in WebIDE +merge_request: !25119 +author: +type: fixed diff --git a/changelogs/unreleased/57564-contributing-button-border.yml b/changelogs/unreleased/57564-contributing-button-border.yml new file mode 100644 index 00000000000..e5875ef1c0f --- /dev/null +++ b/changelogs/unreleased/57564-contributing-button-border.yml @@ -0,0 +1,5 @@ +--- +title: Fix the border style of CONTRIBUTING button when it exists +merge_request: 25124 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/57579-gitlab-project-import-fails-sidekiq-undefined-method-import_jid.yml b/changelogs/unreleased/57579-gitlab-project-import-fails-sidekiq-undefined-method-import_jid.yml new file mode 100644 index 00000000000..f7d6a6c4863 --- /dev/null +++ b/changelogs/unreleased/57579-gitlab-project-import-fails-sidekiq-undefined-method-import_jid.yml @@ -0,0 +1,5 @@ +--- +title: Fix import_jid error on project import +merge_request: 25239 +author: +type: fixed diff --git a/changelogs/unreleased/57589-update-workhorse.yml b/changelogs/unreleased/57589-update-workhorse.yml new file mode 100644 index 00000000000..525913bba4c --- /dev/null +++ b/changelogs/unreleased/57589-update-workhorse.yml @@ -0,0 +1,5 @@ +--- +title: Update Workhorse to v8.3.1 +merge_request: +author: +type: other diff --git a/changelogs/unreleased/57650-remove-tld-validation-from-cluster.yml b/changelogs/unreleased/57650-remove-tld-validation-from-cluster.yml new file mode 100644 index 00000000000..683b007a8a1 --- /dev/null +++ b/changelogs/unreleased/57650-remove-tld-validation-from-cluster.yml @@ -0,0 +1,5 @@ +--- +title: Fixes incorrect TLD validation errors for Kubernetes cluster domain +merge_request: 25262 +author: +type: fixed diff --git a/changelogs/unreleased/57671-fix_merge_request_base_pipeline.yml b/changelogs/unreleased/57671-fix_merge_request_base_pipeline.yml new file mode 100644 index 00000000000..d89819eee60 --- /dev/null +++ b/changelogs/unreleased/57671-fix_merge_request_base_pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Ensure the base pipeline of a Merge Request belongs to its target branch +merge_request: 25226 +author: +type: fixed diff --git a/changelogs/unreleased/57768-remove-vertical-line.yml b/changelogs/unreleased/57768-remove-vertical-line.yml new file mode 100644 index 00000000000..b73b0fa229e --- /dev/null +++ b/changelogs/unreleased/57768-remove-vertical-line.yml @@ -0,0 +1,5 @@ +--- +title: Remove vertical connecting line placeholder from diff discussion notes +merge_request: 25292 +author: +type: fixed diff --git a/changelogs/unreleased/add-title-attribute-to-file-row.yml b/changelogs/unreleased/add-title-attribute-to-file-row.yml new file mode 100644 index 00000000000..c68d3d544e7 --- /dev/null +++ b/changelogs/unreleased/add-title-attribute-to-file-row.yml @@ -0,0 +1,5 @@ +--- +title: add title attribute to display file name +merge_request: 25154 +author: Satoshi Nakamatsu @satoshicano +type: added diff --git a/changelogs/unreleased/changelogs-readme.yml b/changelogs/unreleased/changelogs-readme.yml new file mode 100644 index 00000000000..9f391699575 --- /dev/null +++ b/changelogs/unreleased/changelogs-readme.yml @@ -0,0 +1,5 @@ +--- +title: add readme to changelogs directory +merge_request: 25209 +author: "@glensc" +type: added diff --git a/changelogs/unreleased/diff-tree-resizable.yml b/changelogs/unreleased/diff-tree-resizable.yml new file mode 100644 index 00000000000..7411640aea5 --- /dev/null +++ b/changelogs/unreleased/diff-tree-resizable.yml @@ -0,0 +1,5 @@ +--- +title: Make file tree in merge requests resizable +merge_request: +author: +type: added diff --git a/changelogs/unreleased/fast-destroy-uploads.yml b/changelogs/unreleased/fast-destroy-uploads.yml new file mode 100644 index 00000000000..ee3363a6ae9 --- /dev/null +++ b/changelogs/unreleased/fast-destroy-uploads.yml @@ -0,0 +1,5 @@ +--- +title: File uploads are deleted asynchronously when deleting a project or group. +merge_request: +author: +type: added diff --git a/changelogs/unreleased/feature-gb-enable-ci-persisted-stages-by-default.yml b/changelogs/unreleased/feature-gb-enable-ci-persisted-stages-by-default.yml new file mode 100644 index 00000000000..ad92135d401 --- /dev/null +++ b/changelogs/unreleased/feature-gb-enable-ci-persisted-stages-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Enable persisted pipeline stages by default +merge_request: 25347 +author: +type: performance diff --git a/changelogs/unreleased/filter-note-parameters.yml b/changelogs/unreleased/filter-note-parameters.yml new file mode 100644 index 00000000000..fca2a394820 --- /dev/null +++ b/changelogs/unreleased/filter-note-parameters.yml @@ -0,0 +1,5 @@ +--- +title: Include note in the Rails filter_parameters configuration +merge_request: 25238 +author: +type: other diff --git a/changelogs/unreleased/fix_-56347.yml b/changelogs/unreleased/fix_-56347.yml new file mode 100644 index 00000000000..1d03ed8864c --- /dev/null +++ b/changelogs/unreleased/fix_-56347.yml @@ -0,0 +1,5 @@ +--- +title: Fix overlapping empty-header logo +merge_request: 24868 +author: Jonas L. +type: fixed diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-commit.yml b/changelogs/unreleased/gt-externalize-app-views-projects-commit.yml new file mode 100644 index 00000000000..29dbf2367b7 --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-projects-commit.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/projects/commit` +merge_request: 24668 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/import-go-to-project-cta.yml b/changelogs/unreleased/import-go-to-project-cta.yml new file mode 100644 index 00000000000..ae719f08790 --- /dev/null +++ b/changelogs/unreleased/import-go-to-project-cta.yml @@ -0,0 +1,5 @@ +--- +title: Improve GitHub and Gitea project import table UI +merge_request: 24606 +author: +type: other diff --git a/changelogs/unreleased/improve-performance-for-diverging-commit-counts.yml b/changelogs/unreleased/improve-performance-for-diverging-commit-counts.yml new file mode 100644 index 00000000000..76ff15cba5b --- /dev/null +++ b/changelogs/unreleased/improve-performance-for-diverging-commit-counts.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance for diverging commit counts +merge_request: 24287 +author: +type: performance diff --git a/changelogs/unreleased/ravlen-fix-spaces-unicode.yml b/changelogs/unreleased/ravlen-fix-spaces-unicode.yml new file mode 100644 index 00000000000..fbcbdc53cfe --- /dev/null +++ b/changelogs/unreleased/ravlen-fix-spaces-unicode.yml @@ -0,0 +1,5 @@ +--- +title: Correct non-standard unicode spaces to regular unicode +merge_request: 24795 +author: Marcel Amirault +type: other diff --git a/changelogs/unreleased/remove-second-primary-button-on-wiki-edit.yml b/changelogs/unreleased/remove-second-primary-button-on-wiki-edit.yml new file mode 100644 index 00000000000..045fbbb48b7 --- /dev/null +++ b/changelogs/unreleased/remove-second-primary-button-on-wiki-edit.yml @@ -0,0 +1,5 @@ +--- +title: Remove second primary button on wiki edit +merge_request: 19959 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/rs-admin-user-case-insensitive.yml b/changelogs/unreleased/rs-admin-user-case-insensitive.yml new file mode 100644 index 00000000000..40398c46a1e --- /dev/null +++ b/changelogs/unreleased/rs-admin-user-case-insensitive.yml @@ -0,0 +1,5 @@ +--- +title: Admin section finds users case-insensitively +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-board-user-assigns.yml b/changelogs/unreleased/sh-fix-board-user-assigns.yml new file mode 100644 index 00000000000..89c228107f0 --- /dev/null +++ b/changelogs/unreleased/sh-fix-board-user-assigns.yml @@ -0,0 +1,5 @@ +--- +title: Fix 403 errors when adding an assignee list in project boards +merge_request: 25263 +author: +type: fixed diff --git a/changelogs/unreleased/sh-import-source-branch-github-forks.yml b/changelogs/unreleased/sh-import-source-branch-github-forks.yml new file mode 100644 index 00000000000..b5ea60202c0 --- /dev/null +++ b/changelogs/unreleased/sh-import-source-branch-github-forks.yml @@ -0,0 +1,5 @@ +--- +title: Create the source branch for a GitHub import +merge_request: 25064 +author: +type: fixed diff --git a/changelogs/unreleased/sh-include-project-path-for-internal-api.yml b/changelogs/unreleased/sh-include-project-path-for-internal-api.yml new file mode 100644 index 00000000000..1973049e9e3 --- /dev/null +++ b/changelogs/unreleased/sh-include-project-path-for-internal-api.yml @@ -0,0 +1,5 @@ +--- +title: Include gl_project_path in API /internal/allowed response +merge_request: 25314 +author: +type: other diff --git a/changelogs/unreleased/sh-log-rails-queue-duration.yml b/changelogs/unreleased/sh-log-rails-queue-duration.yml new file mode 100644 index 00000000000..89390aef108 --- /dev/null +++ b/changelogs/unreleased/sh-log-rails-queue-duration.yml @@ -0,0 +1,5 @@ +--- +title: Log queue duration in production_json.log +merge_request: 25075 +author: +type: other diff --git a/changelogs/unreleased/support-only-changes-on-mr-pipelines.yml b/changelogs/unreleased/support-only-changes-on-mr-pipelines.yml new file mode 100644 index 00000000000..fbab898b799 --- /dev/null +++ b/changelogs/unreleased/support-only-changes-on-mr-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: 'Support `only: changes:` on MR pipelines' +merge_request: 24490 +author: Hiroyuki Sato +type: added diff --git a/changelogs/unreleased/syntax-highlighting-again.yml b/changelogs/unreleased/syntax-highlighting-again.yml new file mode 100644 index 00000000000..14223fc0927 --- /dev/null +++ b/changelogs/unreleased/syntax-highlighting-again.yml @@ -0,0 +1,5 @@ +--- +title: Fix suggested changes syntax highlighting +merge_request: 25116 +author: +type: fixed diff --git a/changelogs/unreleased/web-ide-commit-header-icon-alignment-fix.yml b/changelogs/unreleased/web-ide-commit-header-icon-alignment-fix.yml new file mode 100644 index 00000000000..7a6bda1580d --- /dev/null +++ b/changelogs/unreleased/web-ide-commit-header-icon-alignment-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fixed alignment of changed icon in Web IDE +merge_request: +author: +type: fixed diff --git a/config/application.rb b/config/application.rb index 92a3d031c63..49e7f5836e4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -97,7 +97,7 @@ module Gitlab # # NOTE: It is **IMPORTANT** to also update gitlab-workhorse's filter when adding parameters here to not # introduce another security vulnerability: https://gitlab.com/gitlab-org/gitlab-workhorse/issues/182 - config.filter_parameters += [/token$/, /password/, /secret/, /key$/] + config.filter_parameters += [/token$/, /password/, /secret/, /key$/, /^note$/, /^text$/] config.filter_parameters += %i( certificate encrypted_key diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index c897bc30e76..164954d1293 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -23,7 +23,8 @@ unless Sidekiq.server? remote_ip: event.payload[:remote_ip], user_id: event.payload[:user_id], username: event.payload[:username], - ua: event.payload[:ua] + ua: event.payload[:ua], + queue_duration: event.payload[:queue_duration] } gitaly_calls = Gitlab::GitalyClient.get_request_count diff --git a/config/routes/import.rb b/config/routes/import.rb index da5c31d0062..24013eb2c88 100644 --- a/config/routes/import.rb +++ b/config/routes/import.rb @@ -12,13 +12,13 @@ namespace :import do post :personal_access_token get :status get :callback - get :jobs + get :realtime_changes end resource :gitea, only: [:create, :new], controller: :gitea do post :personal_access_token get :status - get :jobs + get :realtime_changes end resource :gitlab, only: [:create], controller: :gitlab do diff --git a/config/webpack.config.js b/config/webpack.config.js index fdf179b007a..cf9e77d2424 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -150,6 +150,7 @@ module.exports = { loader: 'worker-loader', options: { name: '[name].[hash:8].worker.js', + inline: IS_DEV_SERVER, }, }, 'babel-loader', diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile index 530c6638653..63b2f6f5c5c 100644 --- a/danger/changelog/Dangerfile +++ b/danger/changelog/Dangerfile @@ -16,16 +16,12 @@ consider adding any of the %<labels>s labels. #{SEE_DOC} MSG -def ee? - ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md') -end - def ee_changelog?(changelog_path) changelog_path =~ /unreleased-ee/ end def ce_port_changelog?(changelog_path) - ee? && !ee_changelog?(changelog_path) + helper.ee? && !ee_changelog?(changelog_path) end def check_changelog(path) diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile index fff4d0e2582..0cf478b4f89 100644 --- a/danger/documentation/Dangerfile +++ b/danger/documentation/Dangerfile @@ -1,17 +1,6 @@ # frozen_string_literal: true -# All the files/directories that should be reviewed by the Docs team. -DOCS_FILES = [ - 'doc/' -].freeze - -def docs_paths_requiring_review(files) - files.select do |file| - DOCS_FILES.any? { |pattern| file.start_with?(pattern) } - end -end - -docs_paths_to_review = docs_paths_requiring_review(helper.all_changed_files) +docs_paths_to_review = helper.changes_by_category[:docs] unless docs_paths_to_review.empty? message 'This merge request adds or changes files that require a review ' \ diff --git a/danger/plugins/helper.rb b/danger/plugins/helper.rb index f4eb9119266..581c0720083 100644 --- a/danger/plugins/helper.rb +++ b/danger/plugins/helper.rb @@ -1,34 +1,15 @@ # frozen_string_literal: true +require 'net/http' +require 'yaml' + +require_relative '../../lib/gitlab/danger/helper' + module Danger - # Common helper functions for our danger scripts - # If we find ourselves repeating code in our danger files, we might as well put them in here. + # Common helper functions for our danger scripts. See Gitlab::Danger::Helper + # for more details class Helper < Plugin - # Returns a list of all files that have been added, modified or renamed. - # `git.modified_files` might contain paths that already have been renamed, - # so we need to remove them from the list. - # - # Considering these changes: - # - # - A new_file.rb - # - D deleted_file.rb - # - M modified_file.rb - # - R renamed_file_before.rb -> renamed_file_after.rb - # - # it will return - # ``` - # [ 'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb' ] - # ``` - # - # @return [Array<String>] - def all_changed_files - Set.new - .merge(git.added_files.to_a) - .merge(git.modified_files.to_a) - .merge(git.renamed_files.map { |x| x[:after] }) - .subtract(git.renamed_files.map { |x| x[:before] }) - .to_a - .sort - end + # Put the helper code somewhere it can be tested + include Gitlab::Danger::Helper end end diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile new file mode 100644 index 00000000000..6cf54d0f854 --- /dev/null +++ b/danger/roulette/Dangerfile @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +MESSAGE = <<MARKDOWN +## Reviewer roulette + +Changes that require review have been detected! A merge request is normally +reviewed by both a reviewer and a maintainer in its primary category (e.g. +~frontend or ~backend), and by a maintainer in all other categories. +MARKDOWN + +CATEGORY_TABLE_HEADER = <<MARKDOWN + +To spread load more evenly across eligible reviewers, Danger has randomly picked +a candidate for each review slot. Feel free to override this selection if you +think someone else would be better-suited, or the chosen person is unavailable. + +Once you've decided who will review this merge request, mention them as you +normally would! Danger does not (yet?) automatically notify them for you. + +| Category | Reviewer | Maintainer | +| -------- | -------- | ---------- | +MARKDOWN + +UNKNOWN_FILES_MESSAGE = <<MARKDOWN + +These files couldn't be categorised, so Danger was unable to suggest a reviewer. +Please consider creating a merge request to +[add support](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/danger/helper.rb) +for them. +MARKDOWN + +def spin(team, project, category) + reviewers = team.select { |member| member.reviewer?(project, category) } + maintainers = team.select { |member| member.maintainer?(project, category) } + + # TODO: filter out people who are currently not in the office + # TODO: take CODEOWNERS into account? + + reviewer = reviewers[rand(reviewers.size)] + maintainer = maintainers[rand(maintainers.size)] + + "| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |" +end + +def build_list(items) + list = items.map { |filename| "* `#{filename}`" }.join("\n") + + if items.size > 10 + "\n<details>\n\n#{list}\n\n</details>" + else + list + end +end + +changes = helper.changes_by_category + +# Ignore any files that are known but uncategoried. Prompt for any unknown files +changes.delete(:none) +categories = changes.keys - [:unknown] + +unless changes.empty? + team = + begin + helper.project_team + rescue => err + warn("Reviewer roulette failed to load team data: #{err.message}") + [] + end + + # Exclude the MR author from the team for selection purposes + team.delete_if { |teammate| teammate.username == gitlab.mr_author } + + project = helper.project_name + unknown = changes.fetch(:unknown, []) + + rows = categories.map { |category| spin(team, project, category) } + + markdown(MESSAGE) + markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty? + markdown(UNKNOWN_FILES_MESSAGE + build_list(unknown)) unless unknown.empty? +end diff --git a/db/migrate/20180209115333_create_chatops_tables.rb b/db/migrate/20180209115333_create_chatops_tables.rb new file mode 100644 index 00000000000..2cfb71e1007 --- /dev/null +++ b/db/migrate/20180209115333_create_chatops_tables.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class CreateChatopsTables < ActiveRecord::Migration[4.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :ci_pipeline_chat_data, id: :bigserial do |t| + t.integer :pipeline_id, null: false + t.references :chat_name, foreign_key: { on_delete: :cascade }, null: false + t.text :response_url, null: false + + # A pipeline can only contain one row in this table, hence this index is + # unique. + t.index :pipeline_id, unique: true + + t.index :chat_name_id + end + + # rubocop:disable Migration/AddConcurrentForeignKey + add_foreign_key :ci_pipeline_chat_data, :ci_pipelines, + column: :pipeline_id, + on_delete: :cascade + end +end diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index 87992b541b1..33cb19aff9e 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -2,19 +2,6 @@ class LimitsToMysql < ActiveRecord::Migration[4.2] def up return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ - # These columns were removed in 10.3, but this is called from two places: - # 1. A migration run after they were added, but before they were removed. - # 2. A rake task which can be run at any time. - # - # Because of item 2, we need these checks. - if column_exists?(:merge_request_diffs, :st_commits) - change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647 - end - - if column_exists?(:merge_request_diffs, :st_diffs) - change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647 - end - change_column :snippets, :content, :text, limit: 2147483647 change_column :notes, :st_diff, :text, limit: 2147483647 end diff --git a/db/schema.rb b/db/schema.rb index 0f7e9ad4996..25a645562ec 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -421,6 +421,14 @@ ActiveRecord::Schema.define(version: 20190204115450) do t.index ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree end + create_table "ci_pipeline_chat_data", id: :bigserial, force: :cascade do |t| + t.integer "pipeline_id", null: false + t.integer "chat_name_id", null: false + t.text "response_url", null: false + t.index ["chat_name_id"], name: "index_ci_pipeline_chat_data_on_chat_name_id", using: :btree + t.index ["pipeline_id"], name: "index_ci_pipeline_chat_data_on_pipeline_id", unique: true, using: :btree + end + create_table "ci_pipeline_schedule_variables", force: :cascade do |t| t.string "key", null: false t.text "value" @@ -2345,6 +2353,8 @@ ActiveRecord::Schema.define(version: 20190204115450) do add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade + add_foreign_key "ci_pipeline_chat_data", "chat_names", on_delete: :cascade + add_foreign_key "ci_pipeline_chat_data", "ci_pipelines", column: "pipeline_id", on_delete: :cascade add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md index 772e55cef07..94a8803fff1 100644 --- a/doc/administration/auth/authentiq.md +++ b/doc/administration/auth/authentiq.md @@ -50,7 +50,6 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t } ``` - 1. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits. See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq/wiki/Scopes,-callback-url-configuration-and-responses) for more information on scopes and modifiers. diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 0ac73c55580..37e596f198f 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -448,7 +448,6 @@ ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$ba port. - We are assuming the password for the bind_dn user is in bind_dn_password.txt. - ### Invalid credentials when logging in - Make sure the user you are binding with has enough permissions to read the user's diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md index ae38094391b..3136923fa96 100644 --- a/doc/administration/auth/okta.md +++ b/doc/administration/auth/okta.md @@ -140,7 +140,6 @@ Now that the Okta app is configured, it's time to enable it in GitLab. } ``` - 1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations from source respectively for the changes to take effect. diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 60f1911fa2a..a1ac4a2a57c 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -542,7 +542,6 @@ Read more about the Container Registry notifications config options in the >**Note:** Multiple endpoints can be configured for the Container Registry. - **Omnibus GitLab installations** To configure a notification endpoint in Omnibus: diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md index 2ca860bd763..e554c06532e 100644 --- a/doc/administration/high_availability/gitlab.md +++ b/doc/administration/high_availability/gitlab.md @@ -58,6 +58,7 @@ for each GitLab application server in your environment. # Disable components that will not be on the GitLab application server roles ['application_role'] + nginx['enable'] = true # PostgreSQL connection details gitlab_rails['db_adapter'] = 'postgresql' @@ -90,6 +91,8 @@ for each GitLab application server in your environment. certificates are not present, Nginx will fail to start. See [Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) for more information. + > + > **Note:** It is best to set the `uid` and `gid`s prior to the initial reconfigure of GitLab. Omnibus will not recursively `chown` directories if set after the initial reconfigure. ## First GitLab application server @@ -108,8 +111,9 @@ Additional GitLab servers (servers configured **after** the first GitLab server) need some extra configuration. 1. Configure shared secrets. These values can be obtained from the primary - GitLab server in `/etc/gitlab/gitlab-secrets.json`. Add these to - `/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure`. + GitLab server in `/etc/gitlab/gitlab-secrets.json`. Copy this file to the + secondary servers **prior to** running the first `reconfigure` in the steps + above. ```ruby gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860' diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 359de0efadb..28b226cacd5 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -8,7 +8,53 @@ choice already. Some examples including HAProxy (open-source), F5 Big-IP LTM, and Citrix Net Scaler. This documentation will outline what ports and protocols you need to use with GitLab. -## Basic ports +## SSL + +How will you handle SSL in your HA environment? There are several different +options: + +- Each application node terminates SSL +- The load balancer(s) terminate SSL and communication is not secure between + the load balancer(s) and the application nodes +- The load balancer(s) terminate SSL and communication is *secure* between the + load balancer(s) and the application nodes + +### Application nodes terminate SSL + +Configure your load balancer(s) to pass connections on port 443 as 'TCP' rather +than 'HTTP(S)' protocol. This will pass the connection to the application nodes +Nginx service untouched. Nginx will have the SSL certificate and listen on port 443. + +See [Nginx HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) +for details on managing SSL certificates and configuring Nginx. + +### Load Balancer(s) terminate SSL without backend SSL + +Configure your load balancer(s) to use the 'HTTP(S)' protocol rather than 'TCP'. +The load balancer(s) will then be responsible for managing SSL certificates and +terminating SSL. + +Since communication between the load balancer(s) and GitLab will not be secure, +there is some additional configuration needed. See +[Nginx Proxied SSL documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#supporting-proxied-ssl) +for details. + +### Load Balancer(s) terminate SSL with backend SSL + +Configure your load balancer(s) to use the 'HTTP(S)' protocol rather than 'TCP'. +The load balancer(s) will be responsible for managing SSL certificates that +end users will see. + +Traffic will also be secure between the load balancer(s) and Nginx in this +scenario. There is no need to add configuration for proxied SSL since the +connection will be secure all the way. However, configuration will need to be +added to GitLab to configure SSL certificates. See +[Nginx HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) +for details on managing SSL certificates and configuring Nginx. + +## Ports + +### Basic ports | LB Port | Backend Port | Protocol | | ------- | ------------ | --------------- | @@ -16,9 +62,9 @@ you need to use with GitLab. | 443 | 443 | TCP or HTTPS [^1] [^2] | | 22 | 22 | TCP | -## GitLab Pages Ports +### GitLab Pages Ports -If you're using GitLab Pages with custom domain support you will need some +If you're using GitLab Pages with custom domain support you will need some additional port configurations. GitLab Pages requires a separate virtual IP address. Configure DNS to point the `pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the @@ -29,7 +75,7 @@ GitLab Pages requires a separate virtual IP address. Configure DNS to point the | 80 | Varies [^3] | HTTP | | 443 | Varies [^3] | TCP [^4] | -## Alternate SSH Port +### Alternate SSH Port Some organizations have policies against opening SSH port 22. In this case, it may be helpful to configure an alternate SSH hostname that allows users diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index 987a0b9f350..bf5d064d79d 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -855,7 +855,6 @@ To make sure your configuration is correct: You should see a different port after a few seconds delay (the failover/reconnect time). - ## Changelog Changes to Redis HA over time. diff --git a/doc/administration/index.md b/doc/administration/index.md index 12fec2753bf..ef692ea47ee 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -4,24 +4,27 @@ description: 'Learn how to install, configure, update, and maintain your GitLab # Administrator documentation **[CORE ONLY]** -Learn how to administer your GitLab instance (Community Edition and -Enterprise Edition). -Regular users don't have access to GitLab administration tools and settings. +Learn how to administer your self-managed GitLab instance. -GitLab has two product distributions: the open source -[GitLab Community Edition (CE)](https://gitlab.com/gitlab-org/gitlab-ce), -and the open core [GitLab Enterprise Edition (EE)](https://gitlab.com/gitlab-org/gitlab-ee), -available through [different subscriptions](https://about.gitlab.com/pricing/). +GitLab has two product distributions available through [different subscriptions](https://about.gitlab.com/pricing/): -You can [install GitLab CE or GitLab EE](https://about.gitlab.com/installation/ce-or-ee/), -but the features you'll have access to depend on the subscription you choose -(Core, Starter, Premium, or Ultimate). GitLab Community Edition installations -only have access to Core features. +- The open source [GitLab Community Edition (CE)](https://gitlab.com/gitlab-org/gitlab-ce). +- The open core [GitLab Enterprise Edition (EE)](https://gitlab.com/gitlab-org/gitlab-ee). + +You can [install either GitLab CE or GitLab EE](https://about.gitlab.com/installation/ce-or-ee/). +However, the features you'll have access to depend on the subscription you choose +(Core, Starter, Premium, or Ultimate). + +NOTE: **Note:** +GitLab Community Edition installations only have access to Core features. GitLab.com is administered by GitLab, Inc., therefore, only GitLab team members have access to its admin configurations. If you're a GitLab.com user, please check the [user documentation](../user/index.html). +NOTE: **Note:** +Non-administrator users don’t have access to GitLab administration tools and settings. + ## Installing and maintaining GitLab Learn how to install, configure, update, and maintain your GitLab instance. diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index b61c5409a56..d383d1efe70 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -52,7 +52,6 @@ http://localhost:8080/plantuml you can change these defaults by editing the `/etc/tomcat7/server.xml` file. - ## GitLab You need to enable PlantUML integration from Settings under Admin Area. To do diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 698f4caab3a..36dee75bd44 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -23,12 +23,13 @@ requests from the API are logged to a separate file in `api_json.log`. Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example: ```json -{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":[{"key":"param_key","value":"param_value"}],"remote_ip":"18.245.0.1","user_id":1,"username":"admin","gitaly_calls":76} +{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":[{"key":"param_key","value":"param_value"}],"remote_ip":"18.245.0.1","user_id":1,"username":"admin","gitaly_calls":76,"queue_duration": 112.47} ``` In this example, you can see this was a GET request for a specific issue. Notice each line also contains performance data: -1. `duration`: the total time taken to retrieve the request +1. `duration`: total time in milliseconds taken to retrieve the request +1. `queue_duration`: total time in milliseconds that the request was queued inside GitLab Workhorse 1. `view`: total time taken inside the Rails views 1. `db`: total time to retrieve data from the database 1. `gitaly_calls`: total number of calls made to Gitaly @@ -91,6 +92,8 @@ This entry above shows an access to an internal endpoint to check whether an associated SSH key can download the project in question via a `git fetch` or `git clone`. In this example, we see: +1. `duration`: total time in milliseconds taken to retrieve the request +1. `queue_duration`: total time in milliseconds that the request was queued inside GitLab Workhorse 1. `method`: The HTTP method used to make the request 1. `path`: The relative path of the query 1. `params`: Key-value pairs passed in a query string or HTTP body. Sensitive parameters (e.g. passwords, tokens, etc.) are filtered out. diff --git a/doc/administration/operations/cleaning_up_redis_sessions.md b/doc/administration/operations/cleaning_up_redis_sessions.md index 3a35aff8366..b45ca99fd80 100644 --- a/doc/administration/operations/cleaning_up_redis_sessions.md +++ b/doc/administration/operations/cleaning_up_redis_sessions.md @@ -20,7 +20,6 @@ configuration settings if you have used the advanced Redis settings outlined in [Configuration Files Documentation](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/README.md). - First we define a shell function with the proper Redis connection details. ``` diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 10ae8c7dedf..5c809f25fbd 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -11,7 +11,7 @@ description: 'Learn how to administer GitLab Pages.' > - This guide is for Omnibus GitLab installations. If you have installed > GitLab from source, follow the [Pages source installation document](source.md). > - To learn how to use GitLab Pages, read the [user documentation][pages-userguide]. -> - Does NOT support subgroups. See [this issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) for more information and status. +> - Support for subgroup project's websites was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8. This document describes how to set up the _latest_ GitLab Pages feature. Make sure to read the [changelog](#changelog) if you are upgrading to a new GitLab diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 51e1518d73f..4934aaf39f7 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -41,7 +41,6 @@ Registry, etc. ## Hashed Storage - Hashed Storage is the new storage behavior we rolled out with 10.0. Instead of coupling project URL and the folder structure where the repository will be stored on disk, we are coupling a hash, based on the project's ID. This makes diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md index 7d245b3effd..cbc3fbd9473 100644 --- a/doc/administration/restart_gitlab.md +++ b/doc/administration/restart_gitlab.md @@ -137,7 +137,6 @@ If you are using other init systems, like systemd, you can check the [GitLab Recipes][gl-recipes] repository for some unofficial services. These are **not** officially supported so use them at your own risk. - [omnibus-dl]: https://about.gitlab.com/downloads/ "Download the Omnibus packages" [install]: ../install/installation.md "Documentation to install GitLab from source" [mailroom]: reply_by_email.md "Used for replying by email in GitLab issues and merge requests" diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 476ae8e8a76..9dfe085425f 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -149,7 +149,7 @@ _The uploads are stored by default in [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab" [restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab" -[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition Premium" +[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Premium" [ce]: https://about.gitlab.com/gitlab-ce/ "GitLab Community Edition" [ee-3867]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3867 [ce-17358]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17358 diff --git a/doc/api/README.md b/doc/api/README.md index 3b43d195390..89069fe60e1 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -1,94 +1,133 @@ # GitLab API -Automate GitLab via a simple and powerful API. All definitions can be found -under [`/lib/api`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api). +Automate GitLab via a simple and powerful API. The main GitLab API is a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API. Therefore, documentation in this section assumes knowledge of REST concepts. -## API Resources - -The following API resources are available: - -- [Applications](applications.md) -- [Avatar](avatar.md) -- [Award emoji](award_emoji.md) -- [Branches](branches.md) -- [Broadcast messages](broadcast_messages.md) -- [Code snippets](snippets.md) -- [Commits](commits.md) -- [Container Registry](container_registry.md) -- [Custom attributes](custom_attributes.md) -- [Deploy keys](deploy_keys.md), and [deploy keys for multiple projects](deploy_key_multiple_projects.md) -- [Deployments](deployments.md) -- [Discussions](discussions.md) (threaded comments) -- [Environments](environments.md) -- [Events](events.md) -- [Feature flags](features.md) -- Group-related resources, including: - - [Groups](groups.md) - - [Group access requests](access_requests.md) - - [Group badges](group_badges.md) - - [Group issue boards](group_boards.md) - - [Group labels](group_labels.md) - - [Group-level variables](group_level_variables.md) - - [Group members](members.md) - - [Group milestones](group_milestones.md) -- [Issues](issues.md) -- [Issue boards](boards.md) -- [Jobs](jobs.md) -- [Keys](keys.md) -- [Labels](labels.md) -- [Markdown](markdown.md) -- [Merge requests](merge_requests.md) -- [Namespaces](namespaces.md) -- [Notes](notes.md) (comments) -- [Notification settings](notification_settings.md) -- [Pages domains](pages_domains.md) -- [Pipelines](pipelines.md) -- [Pipeline schedules](pipeline_schedules.md) -- [Pipeline triggers](pipeline_triggers.md) and [triggering pipelines](../ci/triggers/README.md) -- Project-related resources, including: - - [Projects](projects.md) including setting Webhooks - - [Project access requests](access_requests.md) - - [Project badges](project_badges.md) - - [Project clusters](project_clusters.md) - - [Project-level variables](project_level_variables.md) - - [Project import/export](project_import_export.md) - - [Project import from GitHub](import.md) - - [Project members](members.md) - - [Project milestones](milestones.md) - - [Project snippets](project_snippets.md) - - [Project templates](project_templates.md) (see also [Templates API Resources](#templates-api-resources)) -- [Protected branches](protected_branches.md) -- [Protected tags](protected_tags.md) -- [Repositories](repositories.md) -- [Repository files](repository_files.md) -- [Repository submodules](repository_submodules.md) -- [Resource label events](resource_label_events.md) -- [Runners](runners.md) -- [Search](search.md) -- [Services](services.md) -- [Settings](settings.md) -- [Sidekiq metrics](sidekiq_metrics.md) -- [System hooks](system_hooks.md) -- [Tags](tags.md) -- [Releases](releases/index.md) -- Release Assets - - [Links](releases/links.md) -- [Todos](todos.md) -- [Users](users.md) -- [Validate CI configuration](lint.md) (linting) -- [Version](version.md) -- [Wikis](wikis.md) - -See also [V3 to V4](v3_to_v4.md). - -### Templates API Resources +## API resources + +Available API resources can be grouped in the following contexts: + +- [Projects](#project-resources). +- [Groups](#group-resources). +- [Standalone](#standalone-resources). + +See also: + +- [V3 to V4](v3_to_v4.md). +- Adding [deploy keys for multiple projects](deploy_key_multiple_projects.md). + +### Project resources + +The following API resources are available in the project context: + +| Resource | Available endpoints | +|:------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Access requests](access_requests.md) | `/projects/:id/access_requests` (also available for groups) | +| [Award emoji](award_emoji.md) | `/projects/:id/issues/.../award_emoji`, `/projects/:id/merge_requests/.../award_emoji`, `/projects/:id/snippets/.../award_emoji` | +| [Branches](branches.md) | `/projects/:id/repository/branches/`, `/projects/:id/repository/merged_branches` | +| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` | +| [Container Registry](container_registry.md) | `/projects/:id/registry/repositories` | +| [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) | +| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) | +| [Deployments](deployments.md) | `/projects/:id/deployments` | +| [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` | +| [Environments](environments.md) | `/projects/:id/environments` | +| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) | +| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) | +| [Issue boards](boards.md) | `/projects/:id/boards` | +| [Jobs](jobs.md) | `/projects/:id/jobs`, `/projects/:id/pipelines/.../jobs` | +| [Labels](labels.md) | `/projects/:id/labels` | +| [Members](members.md) | `/projects/:id/members` (also available for groups) | +| [Merge requests](merge_requests.md) | `/projects/:id/merge_requests` (also available for groups and standalone) | +| [Notes](notes.md) (comments) | `/projects/:id/issues/.../notes`, `/projects/:id/snippets/.../notes`, `/projects/:id/merge_requests/.../notes` | +| [Notification settings](notification_settings.md) | `/projects/:id/notification_settings` (also available for groups and standalone) | +| [Pages domains](pages_domains.md) | `/projects/:id/pages` (also available standalone) | +| [Pipelines](pipelines.md) | `/projects/:id/pipelines` | +| [Pipeline schedules](pipeline_schedules.md) | `/projects/:id/pipeline_schedules` | +| [Pipeline triggers](pipeline_triggers.md) | `/projects/:id/triggers` | +| [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) | +| [Project badges](project_badges.md) | `/projects/:id/badges` | +| [Project clusters](project_clusters.md) | `/projects/:id/clusters` | +| [Project-level variables](project_level_variables.md) | `/projects/:id/variables` | +| [Project import/export](project_import_export.md) | `/projects/:id/export`, `/projects/import`, `/projects/:id/import` | +| [Project milestones](milestones.md) | `/projects/:id/milestones` | +| [Project snippets](project_snippets.md) | `/projects/:id/snippets` | +| [Project templates](project_templates.md) | `/projects/:id/templates` | +| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` | +| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` | +| [Releases](releases/index.md) | `/projects/:id/releases` | +| [Release links](releases/links.md) | `/projects/:id/releases/.../assets/links` | +| [Repositories](repositories.md) | `/projects/:id/repository` | +| [Repository files](repository_files.md) | `/projects/:id/repository/files` | +| [Repository submodules](repository_submodules.md) | `/projects/:id/repository/submodules` | +| [Resource label events](resource_label_events.md) | `/projects/:id/issues/.../resource_label_events`, `/projects/:id/merge_requests/.../resource_label_events` | +| [Runners](runners.md) | `/projects/:id/runners` (also available standalone) | +| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) | +| [Services](services.md) | `/projects/:id/services` | +| [Tags](tags.md) | `/projects/:id/repository/tags` | +| [Wikis](wikis.md) | `/projects/:id/wikis` | + +### Group resources + +The following API resources are available in the group context: + +| Resource | Available endpoints | +|:--------------------------------------------------|:---------------------------------------------------------------------------------| +| [Access requests](access_requests.md) | `/groups/:id/access_requests/` (also available for projects) | +| [Custom attributes](custom_attributes.md) | `/groups/:id/custom_attributes` (also available for projects and users) | +| [Groups](groups.md) | `/groups`, `/groups/.../subgroups` | +| [Group badges](group_badges.md) | `/groups/:id/badges` | +| [Group issue boards](group_boards.md) | `/groups/:id/boards` | +| [Group labels](group_labels.md) | `/groups/:id/labels` | +| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` | +| [Group milestones](group_milestones.md) | `/groups/:id/milestones` | +| [Issues](issues.md) | `/groups/:id/issues` (also available for projects and standalone) | +| [Members](members.md) | `/groups/:id/members` (also available for projects) | +| [Merge requests](merge_requests.md) | `/groups/:id/merge_requests` (also available for projects and standalone) | +| [Notification settings](notification_settings.md) | `/groups/:id/notification_settings` (also available for projects and standalone) | +| [Search](search.md) | `/groups/:id/search` (also available for projects and standalone) | + +### Standalone resources + +The following API resources are available outside of project and group contexts (including `/users`): + +| Resource | Available endpoints | +|:--------------------------------------------------|:------------------------------------------------------------------------| +| [Applications](applications.md) | `/applications` | +| [Avatar](avatar.md) | `/avatar` | +| [Broadcast messages](broadcast_messages.md) | `/broadcast_messages` | +| [Code snippets](snippets.md) | `/snippets` | +| [Custom attributes](custom_attributes.md) | `/users/:id/custom_attributes` (also available for groups and projects) | +| [Deploy keys](deploy_keys.md) | `/deploy_keys` (also available for projects) | +| [Events](events.md) | `/events`, `/users/:id/events` (also available for projects) | +| [Feature flags](features.md) | `/features` | +| [Import repository from GitHub](import.md) | `/import/github` | +| [Issues](issues.md) | `/issues` (also available for groups and projects) | +| [Keys](keys.md) | `/keys` | +| [Markdown](markdown.md) | `/markdown` | +| [Merge requests](merge_requests.md) | `/merge_requests` (also available for groups and projects) | +| [Namespaces](namespaces.md) | `/namespaces` | +| [Notification settings](notification_settings.md) | `/notification_settings` (also available for groups and projects) | +| [Pages domains](pages_domains.md) | `/pages/domains` (also available for projects) | +| [Projects](projects.md) | `/users/:id/projects` (also available for projects) | +| [Runners](runners.md) | `/runners` (also available for projects) | +| [Search](search.md) | `/search` (also available for groups and projects) | +| [Settings](settings.md) | `/application/settings` | +| [Sidekiq metrics](sidekiq_metrics.md) | `/sidekiq` | +| [Suggestions](suggestions.md) | `/suggestions` | +| [System hooks](system_hooks.md) | `/hooks` | +| [Todos](todos.md) | `/todos` | +| [Users](users.md) | `/users` | +| [Validate `.gitlab-ci.yml` file](lint.md) | `/lint` | +| [Version](version.md) | `/version` | + +### Templates API resources Endpoints are available for: - [Dockerfile templates](templates/dockerfiles.md). -- [gitignore templates](templates/gitignores.md). +- [`.gitignore` templates](templates/gitignores.md). - [GitLab CI YAML templates](templates/gitlab_ci_ymls.md). - [Open source license templates](templates/licenses.md). @@ -110,7 +149,7 @@ have been resolved to our satisfaction by the relicensing of the reference implementations under MIT, and the use of the OWF license for the GraphQL specification. -## Compatibility Guidelines +## Compatibility guidelines The HTTP API is versioned using a single number, the current one being 4. This number symbolizes the same as the major version number as described by diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 92936a277ac..1d0e39e6bbf 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -31,7 +31,7 @@ Parameters: | Attribute | Type | Required | Description | |:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| | `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | -| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `issue_iid`/`merge_request_iid`/`snippet_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | Example request: @@ -93,7 +93,7 @@ Parameters: | Attribute | Type | Required | Description | |:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| | `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | -| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `issue_iid`/`merge_request_iid`/`snippet_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | | `award_id` | integer | yes | ID of the award emoji. | Example request: @@ -138,7 +138,7 @@ Parameters: | Attribute | Type | Required | Description | |:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| | `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | -| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `issue_iid`/`merge_request_iid`/`snippet_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | | `name` | string | yes | Name of the emoji without colons. | ```sh @@ -184,7 +184,7 @@ Parameters: | Attribute | Type | Required | Description | |:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| | `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | -| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `issue_iid`/`merge_request_iid`/`snippet_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | | `award_id` | integer | yes | ID of an award emoji. | ```sh @@ -197,7 +197,8 @@ Comments (also known as notes) are a sub-resource of issues, merge requests, and NOTE: **Note:** The examples below describe working with award emoji on comments for an issue, but can be -easily adapted for comments on a merge request. +easily adapted for comments on a merge request or on a snippet. Therefore, you have to replace +`issue_iid` either with `merge_request_iid` or with the `snippet_id`. ### List a comment's award emoji diff --git a/doc/api/commits.md b/doc/api/commits.md index 14742f034e0..8d36ae7d559 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -18,7 +18,6 @@ GET /projects/:id/repository/commits | `all` | boolean | no | Retrieve every commit from the repository | | `with_stats` | boolean | no | Stats about each commit will be added to the response | - ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits" ``` @@ -81,7 +80,6 @@ POST /projects/:id/repository/commits | `author_name` | string | no | Specify the commit author's name | | `stats` | boolean | no | Include commit stats. Default is true | - | `actions[]` Attribute | Type | Required | Description | | --------------------- | ---- | -------- | ----------- | | `action` | string | yes | The action to perform, `create`, `delete`, `move`, `update`, `chmod`| @@ -601,7 +599,6 @@ GET /projects/:id/repository/commits/:sha/merge_requests | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | `sha` | string | yes | The commit SHA - ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits/af5b13261899fb2c0db30abdd0af8b07cb44fdc5/merge_requests" ``` @@ -656,6 +653,46 @@ Example response: ] ``` +## Get GPG signature of a commit + +Get the [GPG signature from a commit](../user/project/repository/gpg_signed_commits/index.md), +if it is signed. For unsigned commits, it results in a 404 response. + +``` +GET /projects/:id/repository/commits/:sha/signature +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +| `sha` | string | yes | The commit hash or name of a repository branch or tag | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/repository/commits/da738facbc19eb2fc2cef57c49be0e6038570352/signature" +``` + +Example response if commit is signed: + +```json +{ + "gpg_key_id": 1, + "gpg_key_primary_keyid": "8254AAB3FBD54AC9", + "gpg_key_user_name": "John Doe", + "gpg_key_user_email": "johndoe@example.com", + "verification_status": "verified", + "gpg_key_subkey_id": null +} +``` + +Example response if commit is unsigned: +```json +{ + "message": "404 GPG Signature Not Found" +} +``` + [ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit" [ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047 [ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026 diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md index c77ed39e4dc..1f17af1f1e9 100644 --- a/doc/api/container_registry.md +++ b/doc/api/container_registry.md @@ -16,7 +16,6 @@ GET /projects/:id/registry/repositories | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | - ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories" ``` diff --git a/doc/api/discussions.md b/doc/api/discussions.md index 79090ea5254..7d68d0ae744 100644 --- a/doc/api/discussions.md +++ b/doc/api/discussions.md @@ -641,7 +641,6 @@ Parameters: curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7?resolved=true ``` - ### Add note to existing merge request discussion Adds a new note to the discussion. diff --git a/doc/api/group_labels.md b/doc/api/group_labels.md index d4715dec192..3d4b099b49e 100644 --- a/doc/api/group_labels.md +++ b/doc/api/group_labels.md @@ -28,6 +28,7 @@ Example response: "id": 7, "name": "bug", "color": "#FF0000", + "text_color" : "#FFFFFF", "description": null, "open_issues_count": 0, "closed_issues_count": 0, @@ -38,6 +39,7 @@ Example response: "id": 4, "name": "feature", "color": "#228B22", + "text_color" : "#FFFFFF", "description": null, "open_issues_count": 0, "closed_issues_count": 0, @@ -73,6 +75,7 @@ Example response: "id": 9, "name": "Feature Proposal", "color": "#FFA500", + "text_color" : "#FFFFFF", "description": "Describes new ideas", "open_issues_count": 0, "closed_issues_count": 0, @@ -108,6 +111,7 @@ Example response: "id": 9, "name": "Feature Idea", "color": "#FFA500", + "text_color" : "#FFFFFF", "description": "Describes new ideas", "open_issues_count": 0, "closed_issues_count": 0, @@ -158,6 +162,7 @@ Example response: "id": 9, "name": "Feature Idea", "color": "#FFA500", + "text_color" : "#FFFFFF", "description": "Describes new ideas", "open_issues_count": 0, "closed_issues_count": 0, @@ -192,6 +197,7 @@ Example response: "id": 9, "name": "Feature Idea", "color": "#FFA500", + "text_color" : "#FFFFFF", "description": "Describes new ideas", "open_issues_count": 0, "closed_issues_count": 0, diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index 7be01ce9c6d..eb974267084 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -48,7 +48,6 @@ Example Response: ] ``` - ## Get single milestone Gets a single group milestone. diff --git a/doc/api/import.md b/doc/api/import.md index 9f8e0d232c6..1deb26e8388 100644 --- a/doc/api/import.md +++ b/doc/api/import.md @@ -15,7 +15,6 @@ POST /import/github | `new_name` | string | no | New repo name | | `target_namespace` | string | yes | Namespace to import repo into | - ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "personal_access_token=abc123&repo_id=12345&target_namespace=root" https://gitlab.example.com/api/v4/import/github ``` @@ -30,4 +29,3 @@ Example response: "full_name": "Administrator / my-repo" } ``` - diff --git a/doc/api/issues.md b/doc/api/issues.md index ed3165d95df..dd328fb8079 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -168,7 +168,6 @@ GET /groups/:id/issues?my_reaction_emoji=star | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | - ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4/issues ``` @@ -568,7 +567,6 @@ PUT /projects/:id/issues/:issue_iid | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | | `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. | - ```bash curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close ``` @@ -822,7 +820,6 @@ Example response: **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. - **Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists. ## Unsubscribe from an issue @@ -990,7 +987,6 @@ Example response: **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. - **Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists. ## Set a time estimate for an issue @@ -1288,7 +1284,6 @@ Example response: ] ``` - ## Participants on issues ``` @@ -1327,7 +1322,6 @@ Example response: ] ``` - ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/doc/api/labels.md b/doc/api/labels.md index aec1a2c7592..9d10d383bf9 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -24,56 +24,66 @@ Example response: "id" : 1, "name" : "bug", "color" : "#d9534f", + "text_color" : "#FFFFFF", "description": "Bug reported by user", "open_issues_count": 1, "closed_issues_count": 0, "open_merge_requests_count": 1, "subscribed": false, - "priority": 10 + "priority": 10, + "is_project_label": true }, { "id" : 4, "color" : "#d9534f", + "text_color" : "#FFFFFF", "name" : "confirmed", "description": "Confirmed issue", "open_issues_count": 2, "closed_issues_count": 5, "open_merge_requests_count": 0, "subscribed": false, - "priority": null + "priority": null, + "is_project_label": true }, { "id" : 7, "name" : "critical", "color" : "#d9534f", + "text_color" : "#FFFFFF", "description": "Critical issue. Need fix ASAP", "open_issues_count": 1, "closed_issues_count": 3, "open_merge_requests_count": 1, "subscribed": false, - "priority": null + "priority": null, + "is_project_label": true }, { "id" : 8, "name" : "documentation", "color" : "#f0ad4e", + "text_color" : "#FFFFFF", "description": "Issue about documentation", "open_issues_count": 1, "closed_issues_count": 0, "open_merge_requests_count": 2, "subscribed": false, - "priority": null + "priority": null, + "is_project_label": false }, { "id" : 9, "color" : "#5cb85c", + "text_color" : "#FFFFFF", "name" : "enhancement", "description": "Enhancement proposal", "open_issues_count": 1, "closed_issues_count": 0, "open_merge_requests_count": 1, "subscribed": true, - "priority": null + "priority": null, + "is_project_label": true } ] ``` @@ -105,12 +115,14 @@ Example response: "id" : 10, "name" : "feature", "color" : "#5843AD", + "text_color" : "#FFFFFF", "description":null, "open_issues_count": 0, "closed_issues_count": 0, "open_merge_requests_count": 0, "subscribed": false, - "priority": null + "priority": null, + "is_project_label": true } ``` @@ -149,7 +161,6 @@ PUT /projects/:id/labels | `description` | string | no | The new description of the label | | `priority` | integer | no | The new priority of the label. Must be greater or equal than zero or `null` to remove the priority. | - ```bash curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/labels" ``` @@ -161,12 +172,14 @@ Example response: "id" : 8, "name" : "docs", "color" : "#8E44AD", + "text_color" : "#FFFFFF", "description": "Documentation", "open_issues_count": 1, "closed_issues_count": 0, "open_merge_requests_count": 2, "subscribed": false, - "priority": null + "priority": null, + "is_project_label": true } ``` @@ -196,12 +209,14 @@ Example response: "id" : 1, "name" : "bug", "color" : "#d9534f", + "text_color" : "#FFFFFF", "description": "Bug reported by user", "open_issues_count": 1, "closed_issues_count": 0, "open_merge_requests_count": 1, "subscribed": true, - "priority": null + "priority": null, + "is_project_label": true } ``` diff --git a/doc/api/lint.md b/doc/api/lint.md index a0307f7081d..71c09d35b8c 100644 --- a/doc/api/lint.md +++ b/doc/api/lint.md @@ -2,7 +2,7 @@ > [Introduced][ce-5953] in GitLab 8.12. -Checks if your .gitlab-ci.yml file is valid. +Checks if your `.gitlab-ci.yml` file is valid. ``` POST /lint diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index d58cd45538d..e176cdffc5f 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -160,7 +160,6 @@ will be the same. In the case of a merge request from a fork, `target_project_id` and `project_id` will be the same and `source_project_id` will be the fork project's ID. - Parameters: | Attribute | Type | Required | Description | @@ -435,6 +434,9 @@ Parameters: "avatar_url": null, "web_url" : "https://gitlab.example.com/admin" }, + "user" : { + "can_merge" : false + } "assignee": { "id": 1, "name": "Administrator", @@ -528,7 +530,6 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `merge_request_iid` (required) - The internal ID of the merge request - ```json [ { @@ -563,7 +564,6 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `merge_request_iid` (required) - The internal ID of the merge request - ```json [ { diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 6e156a14b25..dfe62554852 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -206,4 +206,3 @@ or you can put the token to the Authorization header: ``` curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user ``` - diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md index 4c41350dcdb..70fbe24099f 100644 --- a/doc/api/pages_domains.md +++ b/doc/api/pages_domains.md @@ -8,7 +8,7 @@ The GitLab Pages feature must be enabled to use these endpoints. Find out more a Get a list of all pages domains. The user must have admin permissions. -```http +```text GET /pages/domains ``` @@ -34,7 +34,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap Get a list of project pages domains. The user must have permissions to view pages domains. -```http +```text GET /projects/:id/pages/domains ``` @@ -69,7 +69,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap Get a single project pages domain. The user must have permissions to view pages domains. -```http +```text GET /projects/:id/pages/domains/:domain ``` @@ -110,7 +110,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap Creates a new pages domain. The user must have permissions to create new pages domains. -```http +```text POST /projects/:id/pages/domains ``` @@ -146,7 +146,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain Updates an existing project pages domain. The user must have permissions to change an existing pages domains. -```http +```text PUT /projects/:id/pages/domains/:domain ``` @@ -182,7 +182,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certifi Deletes an existing project pages domain. -```http +```text DELETE /projects/:id/pages/domains/:domain ``` diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index fc91c5741da..5155e996158 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -1,8 +1,8 @@ # Project import/export API -[Introduced][ce-41899] in GitLab 10.6 +> [Introduced][ce-41899] in GitLab 10.6. -[See also the project import/export documentation](../user/project/settings/import_export.md) +See also the [project import/export documentation](../user/project/settings/import_export.md). ## Schedule an export @@ -16,7 +16,7 @@ data file uploads to the final server. If the `upload` params is present, `upload[url]` param is required. (**Note:** This feature was introduced in GitLab 10.7) -```http +```text POST /projects/:id/export ``` @@ -28,8 +28,7 @@ POST /projects/:id/export | `upload[url]` | string | yes | The URL to upload the project | | `upload[http_method]` | string | no | The HTTP method to upload the exported project. Only `PUT` and `POST` methods allowed. Default is `PUT` | - -```console +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/export \ --data "upload[http_method]=PUT" \ --data-urlencode "upload[url]=https://example-bucket.s3.eu-west-3.amazonaws.com/backup?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMBJHN2O62W8IELQ%2F20180312%2Feu-west-3%2Fs3%2Faws4_request&X-Amz-Date=20180312T110328Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=8413facb20ff33a49a147a0b4abcff4c8487cc33ee1f7e450c46e8f695569dbd" @@ -45,7 +44,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab Get the status of export. -```http +```text GET /projects/:id/export ``` @@ -53,7 +52,7 @@ GET /projects/:id/export | --------- | -------------- | -------- | ---------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -```console +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/export ``` @@ -86,7 +85,7 @@ to a web server, etc. Download the finished export. -```http +```text GET /projects/:id/export/download ``` @@ -94,18 +93,18 @@ GET /projects/:id/export/download | --------- | -------------- | -------- | ---------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -```console +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" --remote-header-name --remote-name https://gitlab.example.com/api/v4/projects/5/export/download ``` -```console +```sh ls *export.tar.gz 2017-12-05_22-11-148_namespace_project_export.tar.gz ``` ## Import a file -```http +```text POST /projects/import ``` @@ -124,7 +123,7 @@ cURL to post data using the header `Content-Type: multipart/form-data`. The `file=` parameter must point to a file on your file system and be preceded by `@`. For example: -```console +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "path=api-project" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/projects/import ``` @@ -168,7 +167,7 @@ requests.post(url, headers=headers, data=data, files=files) Get the status of an import. -```http +```text GET /projects/:id/import ``` @@ -176,7 +175,7 @@ GET /projects/:id/import | --------- | -------------- | -------- | ---------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -```console +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/import ``` diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index 8f4640fcbd6..f02674adfe2 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -124,7 +124,6 @@ Parameters: > **Notes:** > [Introduced][ce-29508] in GitLab 9.4. - Available only for admins. ``` diff --git a/doc/api/project_templates.md b/doc/api/project_templates.md index ef98205cd68..3b5b12c8da3 100644 --- a/doc/api/project_templates.md +++ b/doc/api/project_templates.md @@ -101,7 +101,6 @@ GET /projects/:id/templates/:type/:key Example response (Dockerfile): - ```json { "name": "Binary", diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index fa04680d406..a261bb75be5 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -5,6 +5,7 @@ **Valid access levels** The access levels are defined in the `ProtectedRefAccess.allowed_access_levels` method. Currently, these levels are recognized: + ``` 0 => No access 30 => Developer access diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index 943109a3ea9..e7f79a0d359 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -2,7 +2,7 @@ > - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41766) in GitLab 11.7. > - Using this API you can manipulate GitLab's [Release](../../user/project/releases/index.md) entries. -> - For manipulating links as a release asset, see [Release Links API](links.md) +> - For manipulating links as a release asset, see [Release Links API](links.md). ## List Releases @@ -14,7 +14,7 @@ GET /projects/:id/releases | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | --------------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | Example request: @@ -160,7 +160,7 @@ GET /projects/:id/releases/:tag_name | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | --------------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | | `tag_name` | string | yes | The tag where the release will be created from. | Example request: @@ -239,10 +239,10 @@ POST /projects/:id/releases | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | --------------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | | `name` | string | yes | The release name. | | `tag_name` | string | yes | The tag where the release will be created from. | -| `description` | string | yes | The description of the release. You can use [markdown](../user/markdown.md). | +| `description` | string | yes | The description of the release. You can use [markdown](../../user/markdown.md). | | `ref` | string | no | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. | | `assets:links`| array of hash | no | An array of assets links. | | `assets:links:name`| string | no (if `assets:links` specified, it's required) | The name of the link. | @@ -331,10 +331,10 @@ PUT /projects/:id/releases/:tag_name | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | --------------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | | `tag_name` | string | yes | The tag where the release will be created from. | | `name` | string | no | The release name. | -| `description` | string | no | The description of the release. You can use [markdown](../user/markdown.md). | +| `description` | string | no | The description of the release. You can use [markdown](../../user/markdown.md). | Example request: @@ -412,7 +412,7 @@ DELETE /projects/:id/releases/:tag_name | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | --------------------------------------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). | | `tag_name` | string | yes | The tag where the release will be created from. | Example request: diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md index ae99f3bd8b6..fd7b9d6e6e2 100644 --- a/doc/api/releases/links.md +++ b/doc/api/releases/links.md @@ -3,6 +3,7 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41766) in GitLab 11.7. Using this API you can manipulate GitLab's [Release](../../user/project/releases/index.md) links. For manipulating other Release assets, see [Release API](index.md). +GitLab supports links links to `http`, `https`, and `ftp` assets. ## Get links diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 104c64a89ce..681dc72c934 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -119,7 +119,11 @@ would send an archive in ZIP format. Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user -- `sha` (optional) - The commit SHA to download. A tag, branch reference or sha can be used. This defaults to the tip of the default branch if not specified +- `sha` (optional) - The commit SHA to download. A tag, branch reference, or SHA can be used. This defaults to the tip of the default branch if not specified. For example: + + ```sh + curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.com/api/v4/projects/<project_id>/repository/archive?sha=<commit_sha> + ``` ## Compare branches, tags or commits diff --git a/doc/api/search.md b/doc/api/search.md index aaaed7d9956..aa601648b2c 100644 --- a/doc/api/search.md +++ b/doc/api/search.md @@ -281,7 +281,6 @@ Example response: ] ``` - ## Group Search API Search within the specified group. @@ -520,7 +519,6 @@ Search the expression within the specified scope. Currently these scopes are sup The response depends on the requested scope. - ### Scope: issues ```bash diff --git a/doc/api/services.md b/doc/api/services.md index 2a8ce39e570..5d5aa3e5b3e 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -412,7 +412,7 @@ Google GSuite team collaboration tool. Set Hangouts Chat service for a project. ``` -PUT /projects/:id/services/hangouts_chat +PUT /projects/:id/services/hangouts-chat ``` >**Note:** Specific event parameters (e.g. `push_events` flag) were [introduced in v10.4][11435] @@ -438,7 +438,7 @@ Parameters: Delete Hangouts Chat service for a project. ``` -DELETE /projects/:id/services/hangouts_chat +DELETE /projects/:id/services/hangouts-chat ``` ### Get Hangouts Chat service settings @@ -446,7 +446,7 @@ DELETE /projects/:id/services/hangouts_chat Get Hangouts Chat service settings for a project. ``` -GET /projects/:id/services/hangouts_chat +GET /projects/:id/services/hangouts-chat ``` ## Irker (IRC gateway) diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md index 95dcf2d5277..5f2202fa51d 100644 --- a/doc/api/sidekiq_metrics.md +++ b/doc/api/sidekiq_metrics.md @@ -149,4 +149,3 @@ Example response: } } ``` - diff --git a/doc/api/tags.md b/doc/api/tags.md index 23dbf2d9ff7..3177fec618f 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -163,7 +163,6 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `tag_name` (required) - The name of a tag - ## Create a new release Add release notes to the existing git tag. If there diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md index 3804855129c..bf6a914e120 100644 --- a/doc/api/templates/gitignores.md +++ b/doc/api/templates/gitignores.md @@ -1,8 +1,8 @@ -# Gitignores API +# `.gitignore` API -## List gitignore templates +## List `.gitignore` templates -Get all gitignore templates. +Get all `.gitignore` templates. ``` GET /templates/gitignores @@ -99,9 +99,9 @@ Example response: ] ``` -## Single gitignore template +## Single `.gitignore` template -Get a single gitignore template. +Get a single `.gitignore` template. ``` GET /templates/gitignores/:key @@ -109,7 +109,7 @@ GET /templates/gitignores/:key | Attribute | Type | Required | Description | | ---------- | ------ | -------- | ----------- | -| `key` | string | yes | The key of the gitignore template | +| `key` | string | yes | The key of the `.gitignore` template | ```bash curl https://gitlab.example.com/api/v4/templates/gitignores/Ruby diff --git a/doc/api/users.md b/doc/api/users.md index fd8778abb17..b0977810120 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1043,7 +1043,6 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or Please refer to the [Events API documentation](events.md#get-user-contribution-events) - ## Get all impersonation tokens of a user > Requires admin permissions. diff --git a/doc/api/wikis.md b/doc/api/wikis.md index 436d06cfd3a..12f048ac09b 100644 --- a/doc/api/wikis.md +++ b/doc/api/wikis.md @@ -119,7 +119,6 @@ PUT /projects/:id/wikis/:slug | `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, and `asciidoc` | | `slug` | string | yes | The slug (a unique string) of the wiki page | - ```bash curl --request PUT --data "format=rdoc&content=documentation&title=Docs" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/wikis/foo" ``` diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md index 76fdb2eb00a..822d012aa3d 100644 --- a/doc/articles/openshift_and_gitlab/index.md +++ b/doc/articles/openshift_and_gitlab/index.md @@ -1,4 +1,3 @@ --- redirect_to: '../../install/openshift_and_gitlab/index.html' --- - diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md index 8b2ce425cf5..dbe3d9b62c9 100644 --- a/doc/ci/caching/index.md +++ b/doc/ci/caching/index.md @@ -46,7 +46,6 @@ needed to compile the project: with stages and shared artifacts before investing time in changes to the setup. - It's sometimes confusing because the name artifact sounds like something that is only useful outside of the job, like for downloading a final image. But artifacts are also available in between stages within a pipeline. So if you diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index a8c119edaa0..87e86bef44b 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -40,6 +40,7 @@ There's also a collection of repositories with [example projects](https://gitlab ### Miscellaneous +- [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md) - [Using `dpl` as deployment tool](deployment/README.md) - [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md index 6499413baf0..474a481836a 100644 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -112,7 +112,7 @@ on GitLab CI/CD. To set the environment variables, navigate to your project's ![Variable Settings in GitLab](img/cloud_foundry_variables.png) Once set up, GitLab CI/CD will deploy your app to CF at every push to your -repository's deafult branch. To see the build logs or watch your builds running +repository's default branch. To see the build logs or watch your builds running live, navigate to **CI/CD > Pipelines**. CAUTION: **Caution:** diff --git a/doc/ci/examples/deployment/composer-npm-deploy.md b/doc/ci/examples/deployment/composer-npm-deploy.md index 36358515b84..4758ccad5aa 100644 --- a/doc/ci/examples/deployment/composer-npm-deploy.md +++ b/doc/ci/examples/deployment/composer-npm-deploy.md @@ -4,7 +4,6 @@ This guide covers the building dependencies of a PHP project while compiling ass While is possible to create your own image with custom PHP and Node JS versions, for brevity, we will use an existing [Docker image](https://hub.docker.com/r/tetraweb/php/) that contains both PHP and NodeJS installed. - ```yaml image: tetraweb/php ``` diff --git a/doc/ci/examples/end_to_end_testing_webdriverio/img/deployed_dependency_update.png b/doc/ci/examples/end_to_end_testing_webdriverio/img/deployed_dependency_update.png Binary files differnew file mode 100644 index 00000000000..c45d70d7f7a --- /dev/null +++ b/doc/ci/examples/end_to_end_testing_webdriverio/img/deployed_dependency_update.png diff --git a/doc/ci/examples/end_to_end_testing_webdriverio/index.md b/doc/ci/examples/end_to_end_testing_webdriverio/index.md new file mode 100644 index 00000000000..9f3b8d9ad14 --- /dev/null +++ b/doc/ci/examples/end_to_end_testing_webdriverio/index.md @@ -0,0 +1,251 @@ +--- +author: Vincent Tunru +author_gitlab: Vinnl +level: advanced +article_type: user guide +date: 2019-02-18 +description: 'Confidence checking your entire app every time a new feature is added can quickly become repetitive. Learn how to automate it with GitLab CI/CD.' +--- + +# End-to-end testing with GitLab CI/CD and WebdriverIO + +[Review Apps](../../review_apps/index.md) are great: for every merge request +(or branch, for that matter), the new code can be copied and deployed to a fresh production-like live +environment, making it incredibly low-effort to assess the impact of the changes. Thus, when we use a dependency manager like +[Dependencies.io](https://www.dependencies.io/), it can submit a merge request with an updated dependency, +and it will immediately be clear that the application can still be properly built and deployed. After all, you can _see_ it +running! + +<img src="img/deployed_dependency_update.png" alt="dependencies.io" class="image-noshadow"> + +However, looking at the freshly deployed code to check whether it still looks and behaves as +expected is repetitive manual work, which means it is a prime candidate for automation. This is +where automated [end-to-end testing](https://martinfowler.com/bliki/BroadStackTest.html) comes in: +having the computer run through a few simple scenarios that requires the proper functioning of all +layers of your application, from the frontend to the database. In this article, we will discuss how +to write such end-to-end tests, and how to set up GitLab CI/CD to automatically run these tests +against your new code, on a branch-by-branch basis. For the scope of this article, we will walk you +through the process of setting up GitLab CI/CD for end-to-end testing Javascript-based applications +with WebdriverIO, but the general strategy should carry over to other languages. +We assume you are familiar with GitLab, [GitLab CI/CD](../../README.md), [Review Apps](../../review_apps/index.md), and running your app locally, e.g., on `localhost:8000`. + +### What to test + +In the widely-used [testing pyramid strategy](https://martinfowler.com/bliki/TestPyramid.html), end-to-end tests act more like a +safeguard: [most of your code should be covered by +unit tests](https://vincenttunru.com/100-percent-coverage/) that allow you to easily identify the source of a problem, should one occur. Rather, you +will likely want to +[limit the number of end-to-end tests](https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html) +to just enough to give you the confidence that the deployment went as intended, that your +infrastructure is up and running, and that your units of code work well together. + +### Selenium and WebdriverIO + +[Selenium](http://www.seleniumhq.org/) is a piece of software that can control web browsers, e.g., to make them +visit a specific URL or interact with elements on the page. It can be programmatically controlled +from a variety of programming languages. In this article we're going to be using the +[WebdriverIO](http://webdriver.io/) Javascript bindings, but the general concept should carry over +pretty well to +[other programming languages supported by Selenium](http://docs.seleniumhq.org/about/platforms.jsp#programming-languages). + +## Writing tests + +You can write tests using +[several testing frameworks supported by WebdriverIO](http://webdriver.io/guide/testrunner/frameworks.html). +We will be using [Jasmine](https://jasmine.github.io/) here: + +```javascript +describe('A visitor without account', function(){ + it('should be able to navigate to the homepage from the 404 page', function(){ + browser.url('/page-that-does-not-exist'); + + expect(browser.getUrl()).toMatch('page-that-does-not-exist'); + + browser.element('.content a[href="/"]').click(); + + expect(browser.getUrl()).not.toMatch('page-that-does-not-exist'); + }); +}); +``` + +The functions `describe`, `it`, and `browser` are provided by WebdriverIO. Let's break them down one by one. + +The function `describe` allows you to group related tests. This can be useful if, for example, you want to +run the same initialization commands (using [`beforeEach`](https://jasmine.github.io/api/2.9/global.html#beforeEach)) for +multiple tests, such as making sure you are logged in. + +The function `it` defines an individual test. + +[The `browser` object](http://webdriver.io/guide/testrunner/browserobject.html) is WebdriverIO's +special sauce. It provides most of [the WebdriverIO API methods](http://webdriver.io/api.html) that are the key to +steering the browser. In this case, we can use +[`browser.url`](http://webdriver.io/api/protocol/url.html) to visit `/page-that-does-not-exist` to +hit our 404 page. We can then use [`browser.getUrl`](http://webdriver.io/api/property/getUrl.html) +to verify that the current page is indeed at the location we specified. To interact with the page, +we can simply pass CSS selectors to +[`browser.element`](http://webdriver.io/api/protocol/element.html) to get access to elements on the +page and to interact with them - for example, to click on the link back to the home page. + +The simple test shown above +can already give us a lot of confidence if it passes: we know our deployment has succeeded, that the +elements are visible on the page and that actual browsers can interact with it, and that routing +works as expected. And all that in just 10 lines with gratituous whitespace! Add to that succeeding +unit tests and a successfully completed pipeline, and you can be fairly confident that the +dependency upgrade did not break anything without even having to look at your website. + +## Running locally + +We'll get to running the above test in CI/CD in a moment. When writing tests, however, it helps if +you do not have to wait for your pipelines to succeed in order to check whether they do what you +expect them to do. In other words, let's get it to run locally. + +Make sure that your app is running locally. If you use Webpack, +you can use [the Webpack Dev Server WebdriverIO plugin](https://www.npmjs.com/package/wdio-webpack-dev-server-service) +that automatically starts a development server before executing the tests. + +The WebdriverIO documentation has +[an overview of all configuration options](http://webdriver.io/guide/getstarted/configuration.html), but the +easiest way to get started is to start with +[WebdriverIO's default configuration](http://webdriver.io/guide/testrunner/configurationfile.html), which +provides an overview of all available options. The two options that are going to be most relevant now are the +`specs` option, which is an array of paths to your tests, and the `baseUrl` option, which points to where your app is +running. And finally, we will need to tell WebdriverIO in which browsers we would like to run our +tests. This can be configured through the `capabilities` option, which is an array of browser names (e.g. +`firefox` or `chrome`). It is recommended to install +[selenium-assistant](https://googlechromelabs.github.io/selenium-assistant/) to detect all installed +browsers: + +```javascript + const seleniumAssistant = require('selenium-assistant'); + const browsers = seleniumAssistant.getLocalBrowsers(); + config.capabilities = browsers.map(browser => ({ browserName: browser.getId() })); +``` + +But of course, a simple configuration of `config.capabilities = ['firefox']` would work as well. + +If you've installed WebdriverIO as a dependency +(`npm install --save-dev webdriverio`), you can add a line to the `scripts` property in your +`package.json` that runs `wdio` with the path to your configuration file as value, e.g.: + +```javascript + "confidence-check": "wdio wdio.conf.js", +``` + +You can then execute the tests using `npm run confidence-check`, after which you will actually see a +new browser window interacting with your app as you specified. + +## Configuring GitLab CI/CD + +Which brings us to the exciting part: how do we run this in GitLab CI/CD? There are two things we +need to do for this: + +1. Set up [CI/CD jobs](../../yaml/README.md#jobs) that actually have a browser available. +2. Update our WebdriverIO configuration to use those browsers to visit the review apps. + +For the scope of this article, we've defined an additional [CI/CD stage](../../yaml/README.md#stages) +`confidence-check` that is executed _after_ the stage that deploys the review app. It uses the `node:latest` [Docker +image](../../docker/using_docker_images.html). However, WebdriverIO fires up actual browsers +to interact with your application, so we need to install and run them. +Furthermore, WebdriverIO uses Selenium as a common interface to control different browsers, +so we need to install and run Selenium as well. Luckily, the Selenium project provides the Docker images +[standalone-firefox](https://hub.docker.com/r/selenium/standalone-firefox/) and +[standalone-chrome](https://hub.docker.com/r/selenium/standalone-chrome/) that provide just that for +Firefox and Chrome, respectively. (Since Safari and Internet Explorer/Edge are not open source and +not available for Linux, we are unfortunately unable to use those in GitLab CI/CD). + +GitLab CI/CD makes it a breeze to link these images to our `confidence-check` jobs using the +`service` property, which makes the Selenium server available under a hostname based on the image +name. Our job configuration then looks something like this: + +```yaml +e2e:firefox: + stage: confidence-check + services: + - selenium/standalone-firefox + script: + - npm run confidence-check --host=selenium__standalone-firefox +``` + +And likewise for Chrome: + +```yaml +e2e:chrome: + stage: confidence-check + services: + - selenium/standalone-chrome + script: + - npm run confidence-check --host=selenium__standalone-chrome +``` + +Now that we have a job to run the end-to-end tests in, we need to tell WebdriverIO how to connect to +the Selenium servers running alongside it. We've already cheated a bit above by +passing the value of the [`host`](http://webdriver.io/guide/getstarted/configuration.html#host) +option as an argument to `npm run confidence-check` on the command line. +However, we still need to tell WebdriverIO which browser is available for it to use. + +[GitLab CI/CD makes +a number of variables available](../../variables/README.html#predefined-variables-environment-variables) +with information about the current CI job. We can use this information to dynamically set +up our WebdriverIO configuration according to the job that is running. More specifically, we can +tell WebdriverIO what browser to execute the test on depending on the name of the currently running +job. We can do so in WebdriverIO's configuration file, which we named `wdio.conf.js` above: + +```javascript +if(process.env.CI_JOB_NAME) { + dynamicConfig.capabilities = [ + { browserName: process.env.CI_JOB_NAME === 'e2e:chrome' ? 'chrome' : 'firefox' }, + ]; +} +``` + +Likewise, we can tell WebdriverIO where the review app is running - in this example's case, it's on +`<branch name>.flockademic.com`: + +```javascript +if(process.env.CI_COMMIT_REF_SLUG) { + dynamicConfig.baseUrl = `https://${process.env.CI_COMMIT_REF_SLUG}.flockademic.com`; +} +``` + +And we can make sure our local-specific configuration is only used when _not_ running in CI using +`if (!process.env.CI)`. That's basically all the ingredients you need to run your end-to-end tests +on GitLab CI/CD! + +To recap, our `.gitlab-ci.yml` configuration file looks something like this: + +```yaml +image: node:8.10 +stages: + - deploy + - confidence-check +deploy_terraform: + stage: deploy + script: + # Your Review App deployment scripts - for a working example please check https://gitlab.com/Flockademic/Flockademic/blob/5a45f1c2412e93810fab50e2dab8949e2d0633c7/.gitlab-ci.yml#L315 +e2e:firefox: + stage: confidence-check + services: + - selenium/standalone-firefox + script: + - npm run confidence-check --host=selenium__standalone-firefox +e2e:chrome: + stage: confidence-check + services: + - selenium/standalone-chrome + script: + - npm run confidence-check --host=selenium__standalone-chrome +``` + +## What's next + +If you are setting this up for yourself and want to peek at the working configuration of a +production project, see: + +- [Flockademic's `wdio.conf.js`](https://gitlab.com/Flockademic/Flockademic/blob/dev/wdio.conf.js) +- [Flockademic's `.gitlab-ci.yml`](https://gitlab.com/Flockademic/Flockademic/blob/dev/.gitlab-ci.yml) +- [Flockademic's tests](https://gitlab.com/Flockademic/Flockademic/tree/dev/__e2e__) + +There's plenty more that WebdriverIO can do. For example, you can configure a [`screenshotPath`](http://webdriver.io/guide/getstarted/configuration.html#screenshotPath) to tell WebdriverIO to take +a screenshot when tests are failing. Then tell GitLab CI/CD to store those +[artifacts](../../yaml/README.md#artifacts), and you'll be able to see what went +wrong within GitLab. diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index b1ccce744d8..d3b6650b0f4 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -68,7 +68,6 @@ Since we have our app up and running locally, it's time to push the codebase to Let's create [a new project](../../../gitlab-basics/create-project.md) in GitLab named `laravel-sample`. After that, follow the command line instructions displayed on the project's homepage to initiate the repository on our machine and push the first commit. - ```bash cd laravel-sample git init @@ -127,7 +126,6 @@ We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our We also need to add the public key to **Project** > **Settings** > **Repository** as a [Deploy Key](../../../ssh/README.md#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../../gitlab-basics/command-line-commands.md#start-working-on-your-project). - ```bash # As the deployer user on the server # @@ -186,7 +184,6 @@ To use Envoy, we should first install it on our local machine [using the given i The pros of Envoy is that it doesn't require Blade engine, it just uses Blade syntax to define tasks. To start, we create an `Envoy.blade.php` in the root of our app with a simple task to test Envoy. - ```php @servers(['web' => 'remote_username@remote_host']) @@ -220,7 +217,6 @@ Our deployment plan is to clone the latest release from GitLab repository, insta The first step of our deployment process is to define a set of variables within [@setup](https://laravel.com/docs/envoy/#setup) directive. You may change the `app` to your application's name: - ```php ... @@ -246,7 +242,6 @@ You may change the `app` to your application's name: The [@story](https://laravel.com/docs/envoy/#stories) directive allows us define a list of tasks that can be run as a single task. Here we have three tasks called `clone_repository`, `run_composer`, `update_symlinks`. These variables are usable to making our task's codes more cleaner: - ```php ... @@ -618,7 +613,7 @@ Lastly, `when: manual` is used to turn the job from running automatically to a m deploy_production: script: - # Add the private SSH key to the build environment + # Add the private SSH key to the build environment - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - eval $(ssh-agent -s) - ssh-add <(echo "$SSH_PRIVATE_KEY") diff --git a/doc/ci/examples/sast_docker.md b/doc/ci/examples/sast_docker.md index 9f4a63e296d..3a657b3a3d5 100644 --- a/doc/ci/examples/sast_docker.md +++ b/doc/ci/examples/sast_docker.md @@ -1 +1 @@ -This document was moved to [another location](./container_scanning.md).
\ No newline at end of file +This document was moved to [another location](./container_scanning.md). diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md index bf1e61442d4..b7b5c660586 100644 --- a/doc/ci/merge_request_pipelines/index.md +++ b/doc/ci/merge_request_pipelines/index.md @@ -56,6 +56,49 @@ The same tag is shown on the pipeline's details: ![Pipeline's details](img/pipeline_detail.png) +## Excluding certain jobs + +The behavior of the `only: merge_requests` rule is such that _only_ jobs with +that rule are run in the context of a merge request; no other jobs will be run. + +However, you may want to reverse this behaviour, having all of your jobs to run _except_ +for one or two. Consider the following pipeline, with jobs `A`, `B`, and `C`. If you want +all pipelines to always run `A` and `B`, but only want `C` to run for a merge request, +you can configure your `.gitlab-ci.yml` file as follows: + +``` yaml +.only-default: &only-default + only: + - master + - merge_requests + - tags + +A: + <<: *only-default + script: + - ... + +B: + <<: *only-default + script: + - ... + +C: + script: + - ... + only: + - merge_requests +``` + +Since `A` and `B` are getting the `only:` rule to execute in all cases, they will +always run. `C` specifies that it should only run for merge requests, so for any +pipeline except a merge request pipeline, it will not run. + +As you can see, this will help you avoid a lot of boilerplate where you'd need +to add that `only:` rule to all of your jobs in order to make them always run. You +can use this for scenarios like having only pipelines with merge requests get a +Review App set up, helping to save resources. + ## Important notes about merge requests from forked projects Note that the current behavior is subject to change. In the usual contribution diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index c742dc61368..ce55b231666 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -240,21 +240,64 @@ shared Runners will [only run the jobs they are equipped to run](../yaml/README. For instance, at GitLab we have Runners tagged with "rails" if they contain the appropriate dependencies to run Rails test suites. -### Preventing Runners with tags from picking jobs without tags +### Allowing Runners with tags to pick jobs without tags -You can configure a Runner to prevent it from picking -[jobs with tags](../yaml/README.md#tags) when the Runner does not have tags -assigned. This setting can be enabled the first -time you [register a Runner][register] and can be changed afterwards under -each Runner's settings. +When you [register a Runner][register], its default behavior is to **only pick** +[tagged jobs](../yaml/README.md#tags). -To make a Runner pick tagged/untagged jobs: +NOTE: **Note:** +Maintainer [permissions](../../user/permissions.md) are required to change the +Runner settings. -1. Visit your project's **Settings âž” CI/CD** -1. Find the Runner you wish and make sure it's enabled -1. Click the pencil button -1. Check the **Run untagged jobs** option -1. Click **Save changes** for the changes to take effect +To make a Runner pick untagged jobs: + +1. Visit your project's **Settings > CI/CD > Runners**. +1. Find the Runner you want to pick untagged jobs and make sure it's enabled. +1. Click the pencil button. +1. Check the **Run untagged jobs** option. +1. Click the **Save changes** button for the changes to take effect. + +NOTE: **Note:** +The Runner tags list can not be empty when it's not allowed to pick untagged jobs. + +Below are some example scenarios of different variations. + +#### Runner runs only tagged jobs + +The following examples illustrate the potential impact of the Runner being set +to run only tagged jobs. + +Example 1: + +1. The Runner is configured to run only tagged jobs and has the `docker` tag. +1. A job that has a `hello` tag is executed and stuck. + +Example 2: + +1. The Runner is configured to run only tagged jobs and has the `docker` tag. +1. A job that has a `docker` tag is executed and run. + +Example 3: + +1. The Runner is configured to run only tagged jobs and has the `docker` tag. +1. A job that has no tags defined is executed and stuck. + +#### Runner is allowed to run untagged jobs + +The following examples illustrate the potential impact of the Runner being set +to run tagged and untagged jobs. + +Example 1: + +1. The Runner is configured to run untagged jobs and has the `docker` tag. +1. A job that has no tags defined is executed and run. +1. A second job that has a `docker` tag defined is executed and run. + +Example 2: + +1. The Runner is configured to run untagged jobs and has no tags defined. +1. A job that has no tags defined is executed and run. +1. A second job that has a `docker` tag defined is stuck. ### Setting maximum job timeout for a Runner diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 984878b6c9b..31f99cd5e68 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -96,9 +96,9 @@ This can be an array or a multi-line string. jobs, including failed ones. This has to be an array or a multi-line string. The `before_script` and the main `script` are concatenated and run in a single context/container. -The `after_script` is run separately, so depending on the executor, changes done -outside of the working tree might not be visible, e.g. software installed in the -`before_script`. +The `after_script` is run separately. The current working directory is set back to +default. Depending on the executor, changes done outside of the working tree might +not be visible, e.g. software installed in the `before_script`. It's possible to overwrite the globally defined `before_script` and `after_script` if you set it per-job: @@ -423,10 +423,28 @@ connected with merge requests yet, and because GitLab is creating pipelines before an user can create a merge request we don't know a target branch at this point. -Without a target branch, it is not possible to know what the common ancestor is, -thus we always create a job in that case. This feature works best for stable -branches like `master` because in that case GitLab uses the previous commit -that is present in a branch to compare against the latest SHA that was pushed. +#### Using `changes` with `merge_requests` + +With [pipelines for merge requests](../merge_request_pipelines/index.md), +make it possible to define if a job should be created base on files modified +in a merge request. + +For example: + +``` +docker build service one: + script: docker build -t my-service-one-image:$CI_COMMIT_REF_SLUG . + only: + refs: + - merge_requests + changes: + - Dockerfile + - service-one/**/* +``` + +In the scenario above, if you create or update a merge request that changes +either files in `service-one` folder or `Dockerfile`, GitLab creates and triggers +the `docker build service one` job. ## `tags` diff --git a/doc/customization/help_message.md b/doc/customization/help_message.md new file mode 100644 index 00000000000..c2e592d03bf --- /dev/null +++ b/doc/customization/help_message.md @@ -0,0 +1,13 @@ +# GitLab Help custom text + +In larger organizations it is useful to have information about who has the responsibility of maintaining the company GitLab server. + +1. Navigate to the admin area, click on **Preferences** and expand **Help page**. + +1. Under **Help text** fill in the required information about the person(s) administering GitLab or any other information relevant to your needs. + + ![help message](help_message/help_text.png) + +1. After saving the page this information will be shown on the GitLab login page and on the GitLab `/help` page (e.g., <https://gitlab.com/help>). + + ![help text on help page](help_message/help_text_on_help_page.png) diff --git a/doc/customization/help_message/help_text.png b/doc/customization/help_message/help_text.png Binary files differnew file mode 100644 index 00000000000..99697a106bf --- /dev/null +++ b/doc/customization/help_message/help_text.png diff --git a/doc/customization/help_message/help_text_on_help_page.png b/doc/customization/help_message/help_text_on_help_page.png Binary files differnew file mode 100644 index 00000000000..288b4b8c1eb --- /dev/null +++ b/doc/customization/help_message/help_text_on_help_page.png diff --git a/doc/customization/index.md b/doc/customization/index.md new file mode 100644 index 00000000000..71e87b3f111 --- /dev/null +++ b/doc/customization/index.md @@ -0,0 +1,18 @@ +--- +description: Learn how to customize GitLab's appearance for self-managed installations. +--- + +# Customizing GitLab's appearance **[CORE ONLY]** + +For GitLab self-managed instances, it's possible to customize +a few pages. + +Read through the following documents to adjust GitLab's +look and feel to meet your needs: + +- [Custom login page](branded_login_page.md) +- [Custom header and email logo](branded_page_and_email_header.md) +- [Custom favicon](favicon.md) +- [Libravatar](libravatar.md) +- [New project page](new_project_page.md) +- [Custom `/help` message](help_message.md)
\ No newline at end of file diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index 9bd22d3966d..18aaeb5a712 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -38,7 +38,6 @@ For example, you host a service on `http://libravatar.example.com` the `plain_ur `http://libravatar.example.com/avatar/%{hash}?s=%{size}&d=identicon` - ## Omnibus-gitlab example In `/etc/gitlab/gitlab.rb`: @@ -57,10 +56,8 @@ gitlab_rails['gravatar_enabled'] = true gitlab_rails['gravatar_ssl_url'] = "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon" ``` - Run `sudo gitlab-ctl reconfigure` for changes to take effect. - ## Default URL for missing images [Libravatar supports different sets](https://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service. @@ -68,7 +65,6 @@ Run `sudo gitlab-ctl reconfigure` for changes to take effect. In order to use a different set other than `identicon`, replace `&d=identicon` portion of the URL with another supported set. For example, you can use `retro` set in which case the URL would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"` - ## Usage examples #### For Microsoft Office 365 diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md index 0aef0bf5abb..9194f847cdf 100644 --- a/doc/customization/welcome_message.md +++ b/doc/customization/welcome_message.md @@ -1,12 +1 @@ -# Customize the complete sign-in page - -Please see [Branded login page](branded_login_page.md) - -# Add a welcome message to the sign-in page (GitLab Community Edition) - -It is possible to add a markdown-formatted welcome message to your GitLab -sign-in page. Users of GitLab Enterprise Edition should use the [branded login -page feature](branded_login_page.md) instead. - -The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI. -Admin area > Settings +This document was moved to [another location](branded_login_page.md). diff --git a/doc/development/README.md b/doc/development/README.md index d5829e31343..13646cbfe48 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -49,6 +49,7 @@ description: 'Learn how to contribute to GitLab.' - [Working with the GitHub importer](github_importer.md) - [Import/Export development documentation](import_export.md) - [Working with Merge Request diffs](diffs.md) +- [Kubernetes integration guidelines](kubernetes.md) - [Permissions](permissions.md) - [Prometheus metrics](prometheus_metrics.md) - [Guidelines for reusing abstractions](reusing_abstractions.md) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index e65c5f05505..63574b28edc 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -35,7 +35,7 @@ run: redis: (pid 30560) 14274s; run: log: (pid 13807) 2432047s run: redis-exporter: (pid 30946) 14205s; run: log: (pid 13869) 2432045s run: sidekiq: (pid 30953) 14205s; run: log: (pid 13810) 2432047s run: unicorn: (pid 30960) 14204s; run: log: (pid 13809) 2432047s -``` +``` ### Layers @@ -51,11 +51,11 @@ GitLab can be considered to have two layers from a process perspective: - Omnibus configuration options - Layer: Monitoring -[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on. +[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on. ### gitaly -- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration) +- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration) - Layer: Core Service (Data) Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab. (Think GitLab.com or High Availablity Deployments) As of 11.3.0, This service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly). @@ -63,7 +63,7 @@ Gitaly is a service designed by GitLab to remove our need for NFS for Git storag ### gitlab-monitor - Omnibus configuration options -- Layer: Monitoring +- Layer: Monitoring GitLab Monitor is a process disigned in house that allows us to export metrics about GitLab application internals to prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor) @@ -100,14 +100,14 @@ Nginx as an ingress port for all HTTP requests and routes them to the approriate - [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html) - Layer: Monitoring -[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to prometheus for use in Grafana Dashboards. +[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to prometheus for use in Grafana Dashboards. ### postgresql - [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/database.html) - Layer: Core Service (Data) -GitLab packages the popular Database to provide storage for Application meta data and user information. +GitLab packages the popular Database to provide storage for Application meta data and user information. ### prometheus @@ -121,11 +121,11 @@ Prometheus is a time-series tool that helps GitLab administrators expose metrics - [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/redis.html) - Layer: Core Service (Data) -Redis is packaged to provide a place to store: +Redis is packaged to provide a place to store: - session data - temporary cache information -- background job queues. +- background job queues. ### redis-exporter @@ -146,7 +146,7 @@ Sidekiq is a Ruby background job processor that pulls jobs from the redis queue - [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/unicorn.html) - Layer: Core Service (Processor) -[Unicorn](https://bogomips.org/unicorn/) is a Ruby application server that is used to run the core Rails Application that provides the user facing features in GitLab. Often process output you will see this as `bundle` or `config.ru` depending on the GitLab version. +[Unicorn](https://bogomips.org/unicorn/) is a Ruby application server that is used to run the core Rails Application that provides the user facing features in GitLab. Often process output you will see this as `bundle` or `config.ru` depending on the GitLab version. ### Additional Processes @@ -176,7 +176,6 @@ When making a request to an HTTP Endpoint (Think `/users/sign_in`) the request w - unicorn - Since this is a web request, and it needs to access the application it will go to Unicorn. - Postgres/Gitaly/Redis - Depending on the type of request, it may hit these services to store or retreive data. - ### GitLab Git Request Cycle Below we describe the different pathing that HTTP vs. SSH Git requests will take. There is some overlap with the Web Request Cycle but also some differences. diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 25ea2211b64..1b591c7c322 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -23,6 +23,11 @@ one of the [Merge request coaches][team]. If you need assistance with security scans or comments, feel free to include the Security Team (`@gitlab-com/gl-security`) in the review. +The `danger-review` CI job will randomly pick a reviewer and a maintainer for +each area of the codebase that your merge request seems to touch. It only makes +recommendations - feel free to override it if you think someone else is a better +fit! + Depending on the areas your merge request touches, it must be **approved** by one or more [maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer): @@ -132,6 +137,14 @@ If a developer who happens to also be a maintainer was involved in a merge reque as a domain expert and/or reviewer, it is recommended that they are not also picked as the maintainer to ultimately approve and merge it. +Try to review in a timely manner; doing so allows everyone involved in the merge +request to iterate faster as the context is fresh in memory. Further, this +improves contributors' experiences significantly. Reviewers should aim to review +within two working days from the date they were assigned the merge request. If +you don't think you'll be able to review a merge request within that time, let +the author know as soon as possible. When the author of the merge request has not +heard anything after two days, a new reviewer should be assigned. + Maintainers should check before merging if the merge request is approved by the required approvers. diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md index b2c804b2ff0..f00c5ccb9e9 100644 --- a/doc/development/database_debugging.md +++ b/doc/development/database_debugging.md @@ -13,7 +13,6 @@ Available `RAILS_ENV` - `development` (this is your main GDK db) - `test` (used for tests like rspec) - ## Nuke everything and start over If you just want to delete everything and start over with an empty DB (~1 minute): @@ -36,7 +35,6 @@ If your test DB is giving you problems, it is safe to nuke it because it doesn't - `bundle exec rake db:migrate:up VERSION=20170926203418 RAILS_ENV=development`: Set up a migration - `bundle exec rake db:migrate:redo VERSION=20170926203418 RAILS_ENV=development`: Re-run a specific migration - ## Manually access the database Access the database via one of these commands (they all get you to the same place) @@ -54,7 +52,6 @@ bundle exec rails db RAILS_ENV=development - `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run - `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration - ## FAQ ### `ActiveRecord::PendingMigrationError` with Spring diff --git a/doc/development/diffs.md b/doc/development/diffs.md index 43fc125c21d..56e869c21f8 100644 --- a/doc/development/diffs.md +++ b/doc/development/diffs.md @@ -59,28 +59,24 @@ Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] = Gitlab::Git::Di File diffs will be collapsed (but be expandable) if 100 files have already been rendered. - ```ruby Gitlab::Git::DiffCollection.collection_limits[:safe_max_lines] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000 ``` File diffs will be collapsed (but be expandable) if 5000 lines have already been rendered. - ```ruby Gitlab::Git::DiffCollection.collection_limits[:safe_max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] * 5.kilobytes = 500.kilobytes ``` File diffs will be collapsed (but be expandable) if 500 kilobytes have already been rendered. - ```ruby Gitlab::Git::DiffCollection.collection_limits[:max_files] = Commit::DIFF_HARD_LIMIT_FILES = 1000 ``` No more files will be rendered at all if 1000 files have already been rendered. - ```ruby Gitlab::Git::DiffCollection.collection_limits[:max_lines] = Commit::DIFF_HARD_LIMIT_LINES = 50000 ``` @@ -129,4 +125,3 @@ Diff Viewers, which can be found on `models/diff_viewer/*` are classes used to m whether it's a binary, which partial should be used to render it or which File extensions this class accounts for. `DiffViewer::Base` validates _blobs_ (old and new versions) content, extension and file type in order to check if it can be rendered. - diff --git a/doc/development/documentation/site_architecture/global_nav.md b/doc/development/documentation/site_architecture/global_nav.md index 62ca7d6c805..0aa3c41a225 100644 --- a/doc/development/documentation/site_architecture/global_nav.md +++ b/doc/development/documentation/site_architecture/global_nav.md @@ -234,7 +234,7 @@ Examples: ```yaml - category_title: Issues category_url: 'user/project/issues/' - # note that the above URL does not start with a slash and + # note that the above URL does not start with a slash and # does not include index.html at the end docs: @@ -295,7 +295,6 @@ On the other hand, if the user is looking at `/ce/` docs, all the links in the CE nav should link internally to `/ce/` files, except for [`ee-only` docs](#ee-only-docs). - ```html <% if dir != 'ce' %> <a href="/ee/<%= sec[:section_url] %>">...</a> diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index cda66447c2c..7a3a8f25c2d 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -112,7 +112,7 @@ table_display_block: true ## Emphasis - Use double asterisks (`**`) to mark a word or text in bold (`**bold**`). -- Use undescore (`_`) for text in italics (`_italic_`). +- Use underscore (`_`) for text in italics (`_italic_`). - Use greater than (`>`) for blockquotes. ## Punctuation diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index e0985922443..3e85c0e1995 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -831,6 +831,29 @@ should remain working as-is when EE is running without a license. Instead place EE specs in the `ee/spec` folder. +### Code in `spec/factories` + +Use `FactoryBot.modify` to extend factories already defined in CE. + +Note that you cannot define new factories (even nested ones) inside the `FactoryBot.modify` block. You can do so in a +separate `FactoryBot.define` block as shown in the example below: + +```ruby +# ee/spec/factories/notes.rb +FactoryBot.modify do + factory :note do + trait :on_epic do + noteable { create(:epic) } + project nil + end + end +end + +FactoryBot.define do + factory :note_on_epic, parent: :note, traits: [:on_epic] +end +``` + ## JavaScript code in `assets/javascripts/` To separate EE-specific JS-files we should also move the files into an `ee` folder. diff --git a/doc/development/fe_guide/accessibility.md b/doc/development/fe_guide/accessibility.md index 366b220cbb2..df32242a522 100644 --- a/doc/development/fe_guide/accessibility.md +++ b/doc/development/fe_guide/accessibility.md @@ -8,6 +8,5 @@ are useful for testing for potential accessibility problems in GitLab. Accessibility best-practices and more in-depth information is available on [the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools. - [chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools [audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules diff --git a/doc/development/fe_guide/design_patterns.md b/doc/development/fe_guide/design_patterns.md index e05887a19af..0342d16a87c 100644 --- a/doc/development/fe_guide/design_patterns.md +++ b/doc/development/fe_guide/design_patterns.md @@ -74,5 +74,4 @@ new Foo({ container: '.my-element' }); ``` You can find an example of the above in this [class][container-class-example]; - [container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js diff --git a/doc/development/fe_guide/droplab/droplab.md b/doc/development/fe_guide/droplab/droplab.md index e6aa0be671f..2f8c79abde1 100644 --- a/doc/development/fe_guide/droplab/droplab.md +++ b/doc/development/fe_guide/droplab/droplab.md @@ -90,7 +90,6 @@ const list = document.getElementById('list'); droplab.addHook(trigger, list); ``` - ### Dynamic data Adding `data-dynamic` to your dropdown element will enable dynamic list rendering. diff --git a/doc/development/fe_guide/droplab/plugins/input_setter.md b/doc/development/fe_guide/droplab/plugins/input_setter.md index 8e28a41f32e..e229103e462 100644 --- a/doc/development/fe_guide/droplab/plugins/input_setter.md +++ b/doc/development/fe_guide/droplab/plugins/input_setter.md @@ -13,7 +13,6 @@ to update the `input` element with. You can also set the `InputSetter` config to an array of objects, which will allow you to update multiple elements. - ```html <input id="input" value=""> <div id="div" data-selected-id=""></div> diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 3a3cb77f592..86b8972a69e 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -1,7 +1,7 @@ # Frontend Development Guidelines > **Notice:** -We are currently in the process of re-writing our development guide to make it easier to find information. The new guide is still WIP but viewable in [development/new_fe_guide](../new_fe_guide/index.md) +> We are currently in the process of re-writing our development guide to make it easier to find information. The new guide is still WIP but viewable in [development/new_fe_guide](../new_fe_guide/index.md) This document describes various guidelines to ensure consistency and quality across GitLab's frontend team. @@ -32,32 +32,41 @@ For our currently-supported browsers, see our [requirements][requirements]. --- ## [Development Process](development_process.md) + How we plan and execute the work on the frontend. ## [Architecture](architecture.md) + How we go about making fundamental design decisions in GitLab's frontend team or make changes to our frontend development guidelines. ## [Testing](../testing_guide/frontend_testing.md) + How we write frontend tests, run the GitLab test suite, and debug test related issues. ## [Design Patterns](design_patterns.md) + Common JavaScript design patterns in GitLab's codebase. ## [Vue.js Best Practices](vue.md) + Vue specific design patterns and practices. ## [Vuex](vuex.md) + Vuex specific design patterns and practices. ## [Axios](axios.md) + Axios specific practices and gotchas. ## [GraphQL](graphql.md) + How to use GraphQL ## [Icons and Illustrations](icons.md) + How we use SVG for our Icons and Illustrations. ## [Components](components.md) @@ -70,7 +79,7 @@ How we use UI components. ### [JavaScript Style Guide](style_guide_js.md) -We use eslint to enforce our JavaScript style guides. Our guide is based on +We use eslint to enforce our JavaScript style guides. Our guide is based on the excellent [Airbnb][airbnb-js-style-guide] style guide with a few small changes. @@ -81,23 +90,26 @@ Our SCSS conventions which are enforced through [scss-lint][scss-lint]. --- ## [Performance](performance.md) + Best practices for monitoring and maximizing frontend performance. --- ## [Security](security.md) + Frontend security practices. --- ## [Accessibility](accessibility.md) + Our accessibility standards and resources. ## [Internationalization (i18n) and Translations](../i18n/externalization.md) + Frontend internationalization support is described in [this document](../i18n/). The [externalization part of the guide](../i18n/externalization.md) explains the helpers/methods available. - [rails]: http://rubyonrails.org/ [haml]: http://haml.info/ [hamlit]: https://github.com/k0kubun/hamlit @@ -116,6 +128,7 @@ The [externalization part of the guide](../i18n/externalization.md) explains the --- ## [DropLab](droplab/droplab.md) + Our internal `DropLab` dropdown library. - [DropLab](droplab/droplab.md) diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md index e5a383c25f5..0aba38aca8c 100644 --- a/doc/development/fe_guide/performance.md +++ b/doc/development/fe_guide/performance.md @@ -169,7 +169,6 @@ General tips: - [Profiling with Chrome DevTools][google-devtools-profiling] - [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance. - [web-page-test]: http://www.webpagetest.org/ [pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/ [google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en diff --git a/doc/development/fe_guide/security.md b/doc/development/fe_guide/security.md index 19e72c1d368..83bb449e54d 100644 --- a/doc/development/fe_guide/security.md +++ b/doc/development/fe_guide/security.md @@ -77,7 +77,6 @@ Inline styles should be avoided in almost all cases, they should only be used when no alternatives can be found. This allows reusability of styles as well as readability. - [observatory-cli]: https://github.com/mozilla/http-observatory-cli [qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html [secure_headers]: https://github.com/twitter/secureheaders diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 9c614e3468a..60f322c6e75 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -229,7 +229,6 @@ One should apply to be a Vue.js expert by opening an MR when the Merge Request's - Vuex code follows the [documented pattern](./vuex.md#actions-pattern-request-and-receive-namespaces) - Knowledge about the existing Vue and Vuex applications and existing reusable components - [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards [environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index b6161cd6163..67356a12eba 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -124,4 +124,3 @@ Feature.enable(:feature_flag_name) ## Enabling a feature flag (in production) Check how to [roll out changes using feature flags](rolling_out_changes_using_feature_flags.md). - diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md index 597812c8c49..18e4dc2ca0c 100644 --- a/doc/development/file_storage.md +++ b/doc/development/file_storage.md @@ -20,7 +20,6 @@ There are many places where file uploading is used, according to contexts: - LFS Objects - Merge request diffs - ## Disk storage GitLab started saving everything on local disk. While directory location changed from previous versions, diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index ac910e80a89..6054873cb46 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -85,7 +85,7 @@ are very appreciative of the work done by translators and proofreaders! - Spanish - Pedro Garcia - [GitLab](https://gitlab.com/pedgarrod), [Crowdin](https://crowdin.com/profile/breaking_pitt) - Turkish - - Proofreaders needed. + - Ali Demirtaş - [GitLab](https://gitlab.com/alidemirtas), [Crowdin](https://crowdin.com/profile/alidemirtas) - Ukrainian - Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [Crowdin](https://crowdin.com/profile/wheleph) - Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [Crowdin](https://crowdin.com/profile/andruwa13) diff --git a/doc/development/import_export.md b/doc/development/import_export.md index 71db1abb201..e2108605d54 100644 --- a/doc/development/import_export.md +++ b/doc/development/import_export.md @@ -60,12 +60,12 @@ class StuckImportJobsWorker Marked stuck import jobs as failed. JIDs: xyz ``` -``` +``` +-----------+ +-----------------------------------+ |Export Job |--->| Calls ActiveRecord `as_json` and | +-----------+ | `to_json` on all project models | +-----------------------------------+ - + +-----------+ +-----------------------------------+ |Import Job |--->| Loads all JSON in memory, then | +-----------+ | inserts into the DB in batches | @@ -109,13 +109,13 @@ The `AttributeCleaner` removes any prohibited keys: # Removes all `_ids` and other prohibited keys class AttributeCleaner ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] - + def clean @relation_hash.reject do |key, _value| prohibited_key?(key) || !@relation_class.attribute_method?(key) || excluded_key?(key) end.except('id') end - + ... ``` @@ -133,7 +133,7 @@ The `AttributeConfigurationSpec` checks and confirms the addition of new columns SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)} IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} -MSG +MSG ``` The `ModelConfigurationSpec` checks and confirms the addition of new models: @@ -157,7 +157,7 @@ The `ExportFileSpec` detects encrypted or sensitive columns: ```ruby # ExportFileSpec <<-MSG - Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect} + Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect} If you think this information shouldn't get exported, please exclude the model or attribute in IMPORT_EXPORT_CONFIG. @@ -214,7 +214,6 @@ We do not need to bump the version up in any of the following cases: - Remove a column or model (unless there is a DB constraint) - Export new things (such as a new type of upload) - Every time we bump the version, the integration specs will fail and can be fixed with: ```bash @@ -231,7 +230,7 @@ meaning that we want to bump the version up in the next version (or patch releas For example: 1. Add rename to `RelationRenameService` in X.Y -2. Remove it from `RelationRenameService` in X.Y + 1 +2. Remove it from `RelationRenameService` in X.Y + 1 3. Bump Import/Export version in X.Y + 1 ```ruby @@ -270,8 +269,8 @@ included_attributes: user: - :id - :email - ... - + ... + ``` Do not include the following attributes for the models specified: @@ -319,7 +318,7 @@ module Gitlab ensure remove_import_file end - + def restorers [repo_restorer, wiki_restorer, project_tree, avatar_restorer, uploads_restorer, lfs_restorer, statistics_restorer] @@ -346,7 +345,7 @@ module Projects end def save_services - [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, + [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save) end ``` diff --git a/doc/development/kubernetes.md b/doc/development/kubernetes.md new file mode 100644 index 00000000000..4b2d48903ac --- /dev/null +++ b/doc/development/kubernetes.md @@ -0,0 +1,126 @@ +# Kubernetes integration - development guidelines + +This document provides various guidelines when developing for GitLab's +[Kubernetes integration](../user/project/clusters/index.md). + +## Development + +### Architecture + +Some Kubernetes operations, such as creating restricted project +namespaces are performed on the GitLab Rails application. These +operations are performed using a [client library](#client-library). +These operations will carry an element of risk as the operations will be +run as the same user running the GitLab Rails application, see the +[security](#security) section below. + +Some Kubernetes operations, such as installing cluster applications are +performed on one-off pods on the Kubernetes cluster itself. These +installation pods are currently named `install-<application_name>` and +are created within the `gitlab-managed-apps` namespace. + +In terms of code organization, we generally add objects that represent +Kubernetes resources in +[`lib/gitlab/kubernetes`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/kubernetes). + +### Client library + +We use the [`kubeclient`](https://rubygems.org/gems/kubeclient) gem to +perform Kubernetes API calls. As the `kubeclient` gem does not support +different API Groups (e.g. `apis/rbac.authorization.k8s.io`) from a +single client, we have created a wrapper class, +[`Gitlab::Kubernetes::KubeClient`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/kubernetes/kube_client.rb) +that will enable you to achieve this. + +Selected Kubernetes API groups are currently supported. Do add support +for new API groups or methods to +[`Gitlab::Kubernetes::KubeClient`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/kubernetes/kube_client.rb) +if you need to use them. New API groups or API group versions can be +added to `SUPPORTED_API_GROUPS` - internally, this will create an +internal client for that group. New methods can be added as a delegation +to the relevant internal client. + +### Performance considerations + +All calls to the Kubernetes API must be in a background process. Do not +perform Kubernetes API calls within a web request as this will block +unicorn and can easily lead to a Denial Of Service (DoS) attack in GitLab as +the Kubernetes cluster response times are outside of our control. + +The easiest way to ensure your calls happen a background process is to +delegate any such work to happen in a [sidekiq +worker](sidekiq_style_guide.md). + +There are instances where you would like to make calls to Kubernetes and +return the response and as such a background worker does not seem to be +a good fit. For such cases you should make use of [reactive +caching](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/concerns/reactive_caching.rb). +For example: + +```ruby + def calculate_reactive_cache! + { pods: cluster.platform_kubernetes.kubeclient.get_pods } + end + + def pods + with_reactive_cache do |data| + data[:pods] + end + end +``` + +### Testing + +We have some Webmock stubs in +[`KubernetesHelpers`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/helpers/kubernetes_helpers.rb) +which can help with mocking out calls to Kubernetes API in your tests. + +## Security + +### SSRF + +As URLs for Kubernetes clusters are user controlled it is easily +susceptible to Server Side Request Forgery (SSRF) attacks. You should +understand the mitigation strategies if you are adding more API calls to +a cluster. + +Mitigation strategies include: + +1. Not allowing redirects to attacker controller resources: + [`Kubeclient::KubeClient`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/kubernetes/kube_client.rb#) + can be configured to disallow any redirects by passing in + `http_max_redirects: 0` as an option. +1. Not exposing error messages: by doing so, we + prevent attackers from triggering errors to expose results from + attacker controlled requests. For example, we do not expose (or store) + raw error messages: + + ```ruby + rescue Kubernetes::HttpError => e + # bad + # app.make_errored!("Kubernetes error: #{e.message}") + + # good + app.make_errored!("Kubernetes error: #{e.error_code}") + ``` + +## Debugging + +Logs related to the Kubernetes integration can be found in +[kubernetes.log](../administration/logs.md#kuberneteslog). On a local +GDK install, this will be present in `log/kubernetes.log`. + +Some services such as +[`Clusters::Applications::InstallService`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/services/clusters/applications/install_service.rb#L18) +rescues `StandardError` which can make it harder to debug issues in an +development environment. The current workaround is to temporarily +comment out the `rescue` in your local development source. + +You can also follow the installation pod logs to debug issues related to +installation. Once the installation/upgrade is underway, wait for the +pod to be created. Then run the following to obtain the pods logs as +they are written: + +```bash +kubectl logs <pod_name> --follow -n gitlab-managed-apps +``` diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 98b54684d39..bb40c0d32b4 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -48,7 +48,6 @@ various database operations, such as and whether they require downtime and how to work around that whenever possible. - ## Downtime Tagging Every migration must specify if it requires downtime or not, and if it should diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md index d74f141f08f..8441089418e 100644 --- a/doc/development/new_fe_guide/development/testing.md +++ b/doc/development/new_fe_guide/development/testing.md @@ -339,7 +339,6 @@ afterEach(() => { Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps: - ### Browserstack [Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers. diff --git a/doc/development/new_fe_guide/event_tracking.md b/doc/development/new_fe_guide/event_tracking.md new file mode 100644 index 00000000000..1958f1ce528 --- /dev/null +++ b/doc/development/new_fe_guide/event_tracking.md @@ -0,0 +1,74 @@ +# Event Tracking + +We use [Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. + +## Generic tracking function + +In addition to Snowplow's built-in method for tracking page views, we use a generic tracking function which enables us to selectively apply listeners to events. + +The generic tracking function can be imported in EE-specific JS files as follows: + +```javascript +import { trackEvent } from `ee/stats`; +``` + +This gives the user access to the `trackEvent` method, which takes the following parameters: + +| parameter | type | description | required | +| ---------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `category` | string | Describes the page that you're capturing click events on. Unless infeasible, please use the Rails page attribute `document.body.dataset.page` by default. | true | +| `eventName` | string | Describes the action the user is taking. The first word should always describe the action. For example, clicks should be `click` and activations should be `activate`. Use underscores to describe what was acted on. For example, activating a form field would be `activate_form_input`. Clicking on a dropdown is `click_dropdown`. | true | +| `additionalData` | object | Additional data such as `label`, `property`, and `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). | false | + +Read more about instrumentation and the taxonomy in the [Product Handbook](https://about.gitlab.com/handbook/product/feature-instrumentation). + +### Tracking in `.js` and `.vue` files + +The most simple use case is to add tracking programmatically to an event of interest in Javascript. + +The following example demonstrates how to track a click on a button in Javascript by calling the `trackEvent` method explicitly: + +```javascript +import { trackEvent } from `ee/stats`; + +trackEvent('dashboard:projects:index', 'click_button', { + label: 'create_from_template', + property: 'template_preview', + value: 'rails', +}); +``` + +### Tracking in HAML templates + +Sometimes we want to track clicks for multiple elements on a page. Creating event handlers for all elements could soon turn into a tedious task. + +There's a more convenient solution to this problem. When working with HAML templates, we can add `data-track-*` attributes to elements of interest. This way, all elements that have both `data-track-label` and `data-track-event` attributes assigned get marked for event tracking. All we have to do is call the `bindTrackableContainer` method on a container which allows for better scoping. + +Below is an example of `data-track-*` attributes assigned to a button in HAML: + +```ruby +%button.btn{ data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: "my-template" } } +``` + +By calling `bindTrackableContainer('.my-container')`, click handlers get bound to all elements located in `.my-container` provided that they have the necessary `data-track-*` attributes assigned to them. + +```javascript +import Stats from 'ee/stats'; + +document.addEventListener('DOMContentLoaded', () => { + Stats.bindTrackableContainer('.my-container', 'category'); +}); +``` + +The second parameter in `bindTrackableContainer` is optional. If omitted, the value of `document.body.dataset.page` will be used as category instead. + +Below is a list of supported `data-track-*` attributes: + +| attribute | description | required | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `data-track-label` | The `label` in `trackEvent` | true | +| `data-track-event` | The `eventName` in `trackEvent` | true | +| `data-track-property` | The `property` in `trackEvent`. If omitted, an empty string will be used as a default value. | false | +| `data-track-value` | The `value` in `trackEvent`. If omitted, this will be `target.value` or empty string. For checkboxes, the default value being tracked will be the element's checked attribute if `data-track-value` is omitted. | false | + +Since Snowplow is an Enterprise Edition feature, it's necessary to create a CE backport when adding `data-track-*` attributes to HAML templates in most cases. diff --git a/doc/development/new_fe_guide/index.md b/doc/development/new_fe_guide/index.md index bfcca9cec7b..5fd5af252ef 100644 --- a/doc/development/new_fe_guide/index.md +++ b/doc/development/new_fe_guide/index.md @@ -27,6 +27,10 @@ Learn about all the internal JavaScript modules that make up our frontend. Style guides to keep our code consistent. +## [Event Tracking with Snowplow](event_tracking.md) + +How we use Snowplow to track custom events. + ## [Tips](tips.md) Tips from our frontend team to develop more efficiently and effectively. diff --git a/doc/development/performance.md b/doc/development/performance.md index 972c93be817..32970152911 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -336,7 +336,6 @@ the same method won't end up retrieving data from Redis upon every call. When memoizing cached data in an instance variable, make sure to also reset the instance variable when flushing the cache. An example: - ```ruby def first_branch @first_branch ||= cache.fetch(:first_branch) { branches.first } diff --git a/doc/development/policies.md b/doc/development/policies.md index 97424d90fb5..6a3b90f3604 100644 --- a/doc/development/policies.md +++ b/doc/development/policies.md @@ -8,7 +8,6 @@ The policy used is based on the subject's class name - so `Ability.allowed?(user Permissions are broken into two parts: `conditions` and `rules`. Conditions are boolean expressions that can access the database and the environment, while rules are statically configured combinations of expressions and other rules that enable or prevent certain abilities. For an ability to be allowed, it must be enabled by at least one rule, and not prevented by any. - ### Conditions Conditions are defined by the `condition` method, and are given a name and a block. The block will be executed in the context of the policy object - so it can access `@user` and `@subject`, as well as call any methods defined on the policy. Note that `@user` may be nil (in the anonymous case), but `@subject` is guaranteed to be a real instance of the subject class. @@ -66,7 +65,51 @@ Within the rule DSL, you can use: To see how the rules get evaluated into a judgment, it is useful in a console to use `policy.debug(:some_ability)`. This will print the rules in the order they are evaluated. -When a policy is asked whether a particular ability is allowed (`policy.allowed?(:some_ability)`), it does not necessarily have to compute all the conditions on the policy. First, only the rules relevant to that particular ability are selected. Then, the execution model takes advantage of short-circuiting, and attempts to sort rules based on a heuristic of how expensive they will be to calculate. The sorting is dynamic and cache-aware, so that previously calculated conditions will be considered first, before computing other conditions. +For example, let's say you wanted to debug `IssuePolicy`. You might run +the debugger in this way: + +```ruby +user = User.find_by(username: 'john') +issue = Issue.first +policy = IssuePolicy.new(user, issue) +policy.debug(:read_issue) +``` + +An example debug output would look as follows: + +```ruby +- [0] prevent when all?(confidential, ~can_read_confidential) ((@john : Issue/1)) +- [0] prevent when archived ((@john : Project/4)) +- [0] prevent when issues_disabled ((@john : Project/4)) +- [0] prevent when all?(anonymous, ~public_project) ((@john : Project/4)) ++ [32] enable when can?(:reporter_access) ((@john : Project/4)) +``` + +Each line represents a rule that was evaluated. There are a few things to note: + +1. The `-` or `+` symbol indicates whether the rule block was evaluated to be +`false` or `true`, respectively. +2. The number inside the brackets indicates the score. +3. The last part of the line (e.g. `@john : Issue/1`) shows the username +and subject for that rule. + +Here you can see that the first four rules were evaluated `false` for +which user and subject. For example, you can see in the last line that +the rule was activated because the user `root` had at reporter access to +the `Project/4`. + +When a policy is asked whether a particular ability is allowed +(`policy.allowed?(:some_ability)`), it does not necessarily have to +compute all the conditions on the policy. First, only the rules relevant +to that particular ability are selected. Then, the execution model takes +advantage of short-circuiting, and attempts to sort rules based on a +heuristic of how expensive they will be to calculate. The sorting is +dynamic and cache-aware, so that previously calculated conditions will +be considered first, before computing other conditions. + +Note that the score is chosen by a developer via the `score:` parameter +in a `condition` to denote how expensive evaluating this rule would be +relative to other rules. ## Scope diff --git a/doc/development/profiling.md b/doc/development/profiling.md index b7d9f640a3f..f41d635de43 100644 --- a/doc/development/profiling.md +++ b/doc/development/profiling.md @@ -74,7 +74,6 @@ Gitlab::Profiler.print_by_total_time(result, max_percent: 60, min_percent: 2) To print the profile in HTML format, use the following example: - ```ruby result = Gitlab::Profiler.profile('/my-user') @@ -92,7 +91,6 @@ Redash to Looker](https://gitlab.com/gitlab-com/Product/issues/5#note_121194467) We are [currently investigating how to make this data public](https://gitlab.com/meltano/looker/issues/294). - ## Sherlock Sherlock is a custom profiling tool built into GitLab. Sherlock is _only_ diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index ae9bf863419..4cc500ed1b6 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -151,7 +151,6 @@ following: bundle exec rake gemojione:digests ``` - This will update the file `fixtures/emojis/digests.json` based on the currently available Emoji. diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 73893f9dd46..7bdf676be58 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -190,7 +190,7 @@ A check like this could have avoided CVE-2013-4583. ## Properly anchor regular expressions to the start and end of strings -When using regular expressions to validate user input that is passed as an argument to a shell command, make sure to use the `\A` and `\z` anchors that designate the start and end of the string, rather than `^` and `$`, or no anchors at all. +When using regular expressions to validate user input that is passed as an argument to a shell command, make sure to use the `\A` and `\z` anchors that designate the start and end of the string, rather than `^` and `$`, or no anchors at all. If you don't, an attacker could use this to execute commands with potentially harmful effect. @@ -198,7 +198,7 @@ For example, when a project's `import_url` is validated like below, the user cou ```ruby validates :import_url, format: { with: URI.regexp(%w(ssh git http https)) } -# URI.regexp(%w(ssh git http https)) roughly evaluates to /(ssh|git|http|https):(something_that_looks_like_a_url)/ +# URI.regexp(%w(ssh git http https)) roughly evaluates to /(ssh|git|http|https):(something_that_looks_like_a_url)/ ``` Suppose the user submits the following as their import URL: @@ -211,7 +211,6 @@ Since there are no anchors in the used regular expression, the `git:/tmp/lol` in When importing, GitLab would execute the following command, passing the `import_url` as an argument: - ```sh git clone file://git:/tmp/lol ``` diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md index d685cacf9ea..5aa668290b4 100644 --- a/doc/development/testing_guide/ci.md +++ b/doc/development/testing_guide/ci.md @@ -24,7 +24,7 @@ Our current CI parallelization setup is as follows: uploaded to S3. After that, the next pipeline will use the up-to-date -`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. +`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. ### Monitoring diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 3af97717775..85b47f88cc4 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -143,17 +143,21 @@ thousands of unused Docker images.** **How big are the Kubernetes clusters (`review-apps-ce` and `review-apps-ee`)?** > The clusters are currently set up with a single pool of preemptible nodes, - with a minimum of 1 node and a maximum of 100 nodes. + with a minimum of 1 node and a maximum of 50 nodes. **What are the machine running on the cluster?** - > We're currently using `n1-standard-4` (4 vCPUs, 15 GB memory) machines. + > We're currently using `n1-standard-16` (16 vCPUs, 60 GB memory) machines. **How do we secure this from abuse? Apps are open to the world so we need to find a way to limit it to only us.** > This isn't enabled for forks. +## Other resources + +* [Review Apps integration for CE/EE (presentation)](https://docs.google.com/presentation/d/1QPLr6FO4LduROU8pQIPkX1yfGvD13GEJIBOenqoKxR8/edit?usp=sharing) + [charts-1068]: https://gitlab.com/charts/gitlab/issues/1068 [gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/44362587 [gitlab:assets:compile]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511610 diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md index 881629c3bfd..8c2f48fb1e2 100644 --- a/doc/gitlab-basics/create-your-ssh-keys.md +++ b/doc/gitlab-basics/create-your-ssh-keys.md @@ -27,7 +27,6 @@ ![SSH key single page](img/profile_settings_ssh_keys_single_key.png) - >**Note:** Once you add a key, you cannot edit it, only remove it. In case the paste didn't work, you will have to remove the offending key and re-add it. diff --git a/doc/install/README.md b/doc/install/README.md index ae48306e65e..52011526768 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -3,7 +3,7 @@ comments: false description: Read through the GitLab installation methods. --- -# Installation +# Installation **[CORE ONLY]** GitLab can be installed in most GNU/Linux distributions and in a number of cloud providers. To get the best experience from GitLab you need to balance diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index e209a00b38c..51d2a232dc0 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -471,7 +471,6 @@ gitlab_rails['redis_port'] = 6379 Finally, reconfigure GitLab for the change to take effect: - ```sh sudo gitlab-ctl reconfigure ``` diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md index 19a6e46f969..be14858f4c8 100644 --- a/doc/install/azure/index.md +++ b/doc/install/azure/index.md @@ -1,5 +1,5 @@ --- -description: 'Learn how to spin up a +description: 'Learn how to spin up a pre-configured GitLab VM on Microsoft Azure and have your very own private GitLab instance up and running in around 30 minutes.' --- @@ -9,10 +9,10 @@ pre-configured GitLab VM on Microsoft Azure and have your very own private GitLa > > _Ported to the GitLab documentation and updated on 2017-08-24 by [Ian Scorer](https://gitlab.com/iscorer)._ -Azure is Microsoft's business cloud and GitLab is a pre-configured offering on the Azure Marketplace. -Hopefully, you aren't surprised to hear that Microsoft and Azure have embraced open source software -like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that you can spin up a -pre-configured GitLab VM and have your very own private GitLab up and running in around 30 minutes. +Azure is Microsoft's business cloud and GitLab is a pre-configured offering on the Azure Marketplace. +Hopefully, you aren't surprised to hear that Microsoft and Azure have embraced open source software +like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that you can spin up a +pre-configured GitLab VM and have your very own private GitLab up and running in around 30 minutes. Let's get started. ## Getting started @@ -20,40 +20,40 @@ Let's get started. First, you'll need an account on Azure. There are three ways to do this: - If your company (or you) already has an account, then you are ready to go! -- You can also open your own Azure account for free. _At time of writing_, you get $200 -of credit to spend on Azure services for 30 days. You can use this credit to try out paid Azure +- You can also open your own Azure account for free. _At time of writing_, you get $200 +of credit to spend on Azure services for 30 days. You can use this credit to try out paid Azure services, exploring Microsoft's cloud for free. Even after the first 30 days, you never have to pay -anything unless you decide to transition to paid services with a Pay-As-You-Go Azure subscription. -This is a great way to try out Azure and cloud computing, and you can +anything unless you decide to transition to paid services with a Pay-As-You-Go Azure subscription. +This is a great way to try out Azure and cloud computing, and you can [read more in their comprehensive FAQ][Azure-Free-Account-FAQ]. -- If you have an MSDN subscription, you can activate your Azure subscriber benefits. Your MSDN -subscription gives you recurring Azure credits every month, so why not put those credits to use and +- If you have an MSDN subscription, you can activate your Azure subscriber benefits. Your MSDN +subscription gives you recurring Azure credits every month, so why not put those credits to use and try out GitLab right now? ## Working with Azure -Once you have an Azure account, you can get started. Login to Azure using +Once you have an Azure account, you can get started. Login to Azure using [portal.azure.com](https://portal.azure.com) and the first thing you will see is the Dashboard: ![Azure Dashboard](img/azure-dashboard.png) -The Dashboard gives you a quick overview of Azure resources, and from here you can build VMs, +The Dashboard gives you a quick overview of Azure resources, and from here you can build VMs, create SQL Databases, author websites, and perform lots of other cloud tasks. ## Create New VM -The [Azure Marketplace][Azure-Marketplace] is an online store for pre-configured applications and +The [Azure Marketplace][Azure-Marketplace] is an online store for pre-configured applications and services which have been optimized for the cloud by software vendors like GitLab, available on the Azure Marketplace as pre-configured solutions. In this tutorial we will install GitLab Community Edition, but for GitLab Enterprise Edition you can follow the same process. -To begin creating a new GitLab VM, click on the **+ New** icon, type "GitLab" into the search +To begin creating a new GitLab VM, click on the **+ New** icon, type "GitLab" into the search box, and then click the **"GitLab Community Edition"** search result: ![Azure - New - Search for 'GitLab'](img/azure-new-search-gitlab.png) -A new "blade" window will pop-out, where you can read more about the **"GitLab Community Edition"** +A new "blade" window will pop-out, where you can read more about the **"GitLab Community Edition"** offering which is freely available under the MIT Expat License: ![Azure - New - Select 'GitLab Community Edition'](img/azure-new-gitlab-ce.png) @@ -70,18 +70,18 @@ The first items we need to configure are the basic settings of the underlying vi 1. Select a `VM disk type` - either **HDD** _(slower, lower cost)_ or **SSD** _(faster, higher cost)_ 1. Enter a `User name` - e.g. **"gitlab-admin"** 1. Select an `Authentication type`, either **SSH public key** or **Password**: - + > **Note:** if you're unsure which authentication type to use, select **Password** - 1. If you chose **SSH public key** - enter your `SSH public key` into the field provided - _(read the [SSH documentation][GitLab-Docs-SSH] to learn more about how to set up SSH + 1. If you chose **SSH public key** - enter your `SSH public key` into the field provided + _(read the [SSH documentation][GitLab-Docs-SSH] to learn more about how to set up SSH public keys)_ - 1. If you chose **Password** - enter the password you wish to use _(this is the password that you + 1. If you chose **Password** - enter the password you wish to use _(this is the password that you will use later in this tutorial to [SSH] into the VM, so make sure it's a strong password/passphrase)_ 1. Choose the appropriate `Subscription` tier for your Azure account 1. Choose an existing `Resource Group` or create a new one - e.g. **"GitLab-CE-Azure"** - > **Note:** a "Resource group" is a way to group related resources together for easier administration. + > **Note:** a "Resource group" is a way to group related resources together for easier administration. > We chose "GitLab-CE-Azure", but your resource group can have the same name as your VM. 1. Choose a `Location` - if you're unsure, select the default location @@ -94,23 +94,23 @@ Check the settings you have entered, and then click **"OK"** when you're ready t ## Size -Next, you need to choose the size of your VM - selecting features such as the number of CPU cores, +Next, you need to choose the size of your VM - selecting features such as the number of CPU cores, the amount of RAM, the size of storage (and its speed), etc. -> **Note:** in common with other cloud vendors, Azure operates a resource/usage pricing model, i.e. -the more resources your VM consumes the more it will cost you to run, so make your selection +> **Note:** in common with other cloud vendors, Azure operates a resource/usage pricing model, i.e. +the more resources your VM consumes the more it will cost you to run, so make your selection carefully. You'll see that Azure provides an _estimated_ monthly cost beneath each VM Size to help guide your selection. -The default size - the lowest cost **"DS1_V2 Standard"** VM - meets the minimum system requirements -to run a small GitLab environment for testing and evaluation purposes, and so we're going to go +The default size - the lowest cost **"DS1_V2 Standard"** VM - meets the minimum system requirements +to run a small GitLab environment for testing and evaluation purposes, and so we're going to go ahead and select this one, but please choose the size which best meets your own requirements: ![Azure - Create Virtual Machine - Size](img/azure-create-virtual-machine-size.png) -> **Note:** be aware that whilst your VM is active (known as "allocated"), it will incur -"compute charges" which, ultimately, you will be billed for. So, even if you're using the -free trial credits, you'll likely want to learn +> **Note:** be aware that whilst your VM is active (known as "allocated"), it will incur +"compute charges" which, ultimately, you will be billed for. So, even if you're using the +free trial credits, you'll likely want to learn [how to properly shutdown an Azure VM to save money][Azure-Properly-Shutdown-VM]. Go ahead and click your chosen size, then click **"Select"** when you're ready to proceed to the @@ -118,8 +118,8 @@ next step. ## Settings -On the next blade, you're asked to configure the Storage, Network and Extension settings. -We've gone with the default settings as they're sufficient for test-driving GitLab, but please +On the next blade, you're asked to configure the Storage, Network and Extension settings. +We've gone with the default settings as they're sufficient for test-driving GitLab, but please choose the settings which best meet your own requirements: ![Azure - Create Virtual Machine - Settings](img/azure-create-virtual-machine-settings.png) @@ -128,80 +128,80 @@ Review the settings and then click **"OK"** when you're ready to proceed to the ## Purchase -The Purchase page is the last step and here you will be presented with the price per hour for your -new VM. You'll be billed only for the VM itself (e.g. "Standard DS1 v2") because the +The Purchase page is the last step and here you will be presented with the price per hour for your +new VM. You'll be billed only for the VM itself (e.g. "Standard DS1 v2") because the **"GitLab Community Edition"** marketplace solution is free to use at 0 USD/hr: ![Azure - Create Virtual Machine - Purchase](img/azure-create-virtual-machine-purchase.png) -> **Note:** at this stage, you can review and modify the any of the settings you have made during all +> **Note:** at this stage, you can review and modify the any of the settings you have made during all previous steps, just click on any of the four steps to re-open them. When you have read and agreed to the terms of use and are ready to proceed, click **"Purchase"**. ## Deployment -At this point, Azure will begin deploying your new VM. The deployment process will take a few +At this point, Azure will begin deploying your new VM. The deployment process will take a few minutes to complete, with progress displayed on the **"Deployment"** blade: ![Azure - Create Virtual Machine - Deployment](img/azure-create-virtual-machine-deployment.png) -Once the deployment process is complete, the new VM and its associated resources will be displayed +Once the deployment process is complete, the new VM and its associated resources will be displayed on the Azure Dashboard (you may need to refresh the page): ![Azure - Dashboard - All resources](img/azure-dashboard-running-resources.png) -The new VM can also be accessed by clicking the `All resources` or `Virtual machines` icons in the +The new VM can also be accessed by clicking the `All resources` or `Virtual machines` icons in the Azure Portal sidebar navigation menu. ## Set up a domain name -The VM will have a public IP address (static by default), but Azure allows us to assign a friendly +The VM will have a public IP address (static by default), but Azure allows us to assign a friendly DNS name to the VM, so let's go ahead and do that. -From the Dashboard, click on the **"GitLab-CE"** tile to open the management blade for the new VM. +From the Dashboard, click on the **"GitLab-CE"** tile to open the management blade for the new VM. The public IP address that the VM uses is shown in the 'Essentials' section: ![Azure - VM - Management - Public IP Address](img/azure-vm-management-public-ip.png) -Click on the public IP address - which should open the **"Public IP address - Configuration"** blade, -then click on **"Configuration"** (under "Settings"). Now enter a friendly DNS name for your instance +Click on the public IP address - which should open the **"Public IP address - Configuration"** blade, +then click on **"Configuration"** (under "Settings"). Now enter a friendly DNS name for your instance in the `DNS name label` field: ![Azure - VM - Domain Name](img/azure-vm-domain-name.png) -In the screenshot above, you'll see that we've set the `DNS name label` to **"gitlab-ce-test"**. -This will make our VM accessible at `gitlab-ce-test.centralus.cloudapp.azure.com` -_(the full domain name of your own VM will be different, of course)_. +In the screenshot above, you'll see that we've set the `DNS name label` to **"gitlab-ce-test"**. +This will make our VM accessible at `gitlab-ce-test.centralus.cloudapp.azure.com` +_(the full domain name of your own VM will be different, of course)_. Click **"Save"** for the changes to take effect. -> **Note:** if you want to use your own domain name, you will need to add a DNS `A` record at your -domain registrar which points to the public IP address of your Azure VM. If you do this, you'll need -to make sure your VM is configured to use a _static_ public IP address (i.e. not a _dynamic_ one) -or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP +> **Note:** if you want to use your own domain name, you will need to add a DNS `A` record at your +domain registrar which points to the public IP address of your Azure VM. If you do this, you'll need +to make sure your VM is configured to use a _static_ public IP address (i.e. not a _dynamic_ one) +or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP address. Read [IP address types and allocation methods in Azure][Azure-IP-Address-Types] to learn more. ## Let's open some ports! -At this stage you should have a running and fully operational VM. However, none of the services on -your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the +At this stage you should have a running and fully operational VM. However, none of the services on +your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the necessary ports to enable access to those services. -Ports are opened by adding _security rules_ to the **"Network security group"** (NSG) which our VM -has been assigned to. If you followed the process above, then Azure will have automatically created -an NSG named `GitLab-CE-nsg` and assigned the `GitLab-CE` VM to it. +Ports are opened by adding _security rules_ to the **"Network security group"** (NSG) which our VM +has been assigned to. If you followed the process above, then Azure will have automatically created +an NSG named `GitLab-CE-nsg` and assigned the `GitLab-CE` VM to it. -> **Note:** if you gave your VM a different name then the NSG automatically created by Azure will +> **Note:** if you gave your VM a different name then the NSG automatically created by Azure will also have a different name - the name you have your VM, with `-nsg` appended to it. -You can navigate to the NSG settings via many different routes in the Azure Portal, but one of the -simplest ways is to go to the Azure Dashboard, and then click on the Network Security Group listed +You can navigate to the NSG settings via many different routes in the Azure Portal, but one of the +simplest ways is to go to the Azure Dashboard, and then click on the Network Security Group listed in the **"All resources"** tile: ![Azure - Dashboard - All resources - Network security group](img/azure-dashboard-highlight-nsg.png) -With the **"Network security group"** blade open, click on **"Inbound security rules"** under +With the **"Network security group"** blade open, click on **"Inbound security rules"** under **"Settings"**: ![Azure - Network security group - Inbound security rules](img/azure-nsg-inbound-sec-rules-highlight.png) @@ -212,18 +212,18 @@ Next, click **"Add"**: ### Which ports to open? -Like all servers, our VM will be running many services. However, we want to open up the correct +Like all servers, our VM will be running many services. However, we want to open up the correct ports to enable public internet access to two services in particular: -1. **HTTP** (port 80) - opening port 80 will enable our VM to respond to HTTP requests, allowing +1. **HTTP** (port 80) - opening port 80 will enable our VM to respond to HTTP requests, allowing public access to the instance of GitLab running on our VM. -1. **SSH** (port 22) - opening port 22 will enable our VM to respond to SSH connection requests, -allowing public access (with authentication) to remote terminal sessions +1. **SSH** (port 22) - opening port 22 will enable our VM to respond to SSH connection requests, +allowing public access (with authentication) to remote terminal sessions _(you'll see why we need [SSH] access to our VM [later on in this tutorial](#maintaining-your-gitlab-instance))_ ### Open HTTP on Port 80 -In the **"Add inbound security rule"** blade, let's open port 80 so that our VM will accept HTTP +In the **"Add inbound security rule"** blade, let's open port 80 so that our VM will accept HTTP connections: ![Azure - Add inbound security rules - HTTP](img/azure-add-inbound-sec-rule-http.png) @@ -235,7 +235,7 @@ connections: ### Open SSH on Port 22 -Repeat the above process, adding a second Inbound security rule to open port 22, enabling our VM to +Repeat the above process, adding a second Inbound security rule to open port 22, enabling our VM to accept [SSH] connections: ![Azure - Add inbound security rules - SSH](img/azure-add-inbound-sec-rule-ssh.png) @@ -245,16 +245,15 @@ accept [SSH] connections: 1. Make sure the `Action` is set to **Allow** 1. Click **"OK"** - -It will take a moment for Azure to add each new Inbound Security Rule (and you may need to click on -**"Inbound security rules"** to refresh the list), but once completed, you should see the two new +It will take a moment for Azure to add each new Inbound Security Rule (and you may need to click on +**"Inbound security rules"** to refresh the list), but once completed, you should see the two new rules in the list: ![Azure - Inbound security rules - List](img/azure-inbound-sec-rules-list.png) ## Connecting to GitLab -Use the domain name you set up earlier (or the public IP address) to visit your new GitLab instance -in your browser. If everything has gone according to plan you should be presented with the +Use the domain name you set up earlier (or the public IP address) to visit your new GitLab instance +in your browser. If everything has gone according to plan you should be presented with the following page, asking you to set a _new_ password for the administrator account automatically created by GitLab: @@ -262,26 +261,26 @@ created by GitLab: Enter your _new_ password into both form fields, and then click **"Change your password"**. -Once you have changed the password you will be redirected to the GitLab login page. Use `root` as +Once you have changed the password you will be redirected to the GitLab login page. Use `root` as the username, enter the new password you set in the previous step, and then click **"Sign in"**: ![GitLab - Login](img/gitlab-login.png) ### Success? -After signing in successfully, you should see the GitLab Projects page displaying a +After signing in successfully, you should see the GitLab Projects page displaying a **"Welcome to GitLab!"** message: ![GitLab - Projects Page](img/gitlab-home.png) -If so, you now have a working GitLab instance on your own private Azure VM. **Congratulations!** +If so, you now have a working GitLab instance on your own private Azure VM. **Congratulations!** ## Creating your first GitLab project -You can skip this section if you are familiar with Git and GitLab. Otherwise, let's create our first +You can skip this section if you are familiar with Git and GitLab. Otherwise, let's create our first project. From the Welcome page, click **"New Project"**. -Let's give our project a name and a description, and then accept the default values for everything +Let's give our project a name and a description, and then accept the default values for everything else: 1. Enter **"demo"** into the `Project path` project name field @@ -290,12 +289,12 @@ else: ![GitLab - New Project](img/gitlab-new-project.png) -Once the new project has been created (which should only take a moment), you'll be redirected to +Once the new project has been created (which should only take a moment), you'll be redirected to homepage for the project: ![GitLab - Empty Project](img/gitlab-project-home-empty.png) -If you scroll further down the project's home page, you'll see some basic instructions on how to +If you scroll further down the project's home page, you'll see some basic instructions on how to set up a local clone of your new repository and push and pull from it: ![GitLab - Empty Project - Basic Instructions](img/gitlab-project-home-instructions.png) @@ -304,50 +303,50 @@ set up a local clone of your new repository and push and pull from it: ## Maintaining your GitLab instance -It's important to keep your GitLab environment up-to-date. The GitLab team is constantly making -enhancements and occasionally you may need to update for security reasons. So let's review how to -update GitLab. +It's important to keep your GitLab environment up-to-date. The GitLab team is constantly making +enhancements and occasionally you may need to update for security reasons. So let's review how to +update GitLab. ### Checking our current version To check which version of GitLab we're currently running, click on the "Admin Area" link - it's the -the wrench icon displayed in the top-right, next to the search box. +the wrench icon displayed in the top-right, next to the search box. -In the following screenshot you can see an **"update asap"** notification message in the top-right. -This particular message indicates that there is a newer version of GitLab available which contains +In the following screenshot you can see an **"update asap"** notification message in the top-right. +This particular message indicates that there is a newer version of GitLab available which contains one or more security fixes: ![GitLab - update asap](img/gitlab-admin-area.png) -Under the **"Components"** section, we can see that our VM is currently running version `8.6.5` of -GitLab. This is the version of GitLab which was contained in the Azure Marketplace -**"GitLab Community Edition"** offering we used to build the VM when we wrote this tutorial. +Under the **"Components"** section, we can see that our VM is currently running version `8.6.5` of +GitLab. This is the version of GitLab which was contained in the Azure Marketplace +**"GitLab Community Edition"** offering we used to build the VM when we wrote this tutorial. -> **Note:** The version of GitLab in your own VM instance may well be different, but the update +> **Note:** The version of GitLab in your own VM instance may well be different, but the update process will still be the same. ### Connect via SSH -To perform an update, we need to connect directly to our Azure VM instance and run some commands -from the terminal. Our Azure VM is actually a server running Linux (Ubuntu), so we'll need to -connect to it using SSH ([Secure Shell][SSH]). +To perform an update, we need to connect directly to our Azure VM instance and run some commands +from the terminal. Our Azure VM is actually a server running Linux (Ubuntu), so we'll need to +connect to it using SSH ([Secure Shell][SSH]). -If you're running Windows, you'll need to connect using [PuTTY] or an equivalent Windows SSH client. -If you're running Linux or macOS, then you already have an SSH client installed. +If you're running Windows, you'll need to connect using [PuTTY] or an equivalent Windows SSH client. +If you're running Linux or macOS, then you already have an SSH client installed. -> **Note:** -> - Remember that you will need to login with the username and password you specified +> **Note:** +> - Remember that you will need to login with the username and password you specified > [when you created](#basics) your Azure VM -> - If you need to reset your VM password, read +> - If you need to reset your VM password, read > [how to reset SSH credentials for a user on an Azure VM][Azure-Troubleshoot-SSH-Connection]. #### SSH from the command-line -If you're running [SSH] from the command-line (terminal), then type in the following command to -connect to your VM, substituting `username` and `your-azure-domain-name.com` for the correct values. +If you're running [SSH] from the command-line (terminal), then type in the following command to +connect to your VM, substituting `username` and `your-azure-domain-name.com` for the correct values. -Again, remember that your Azure VM domain name will be the one you -[set up previously in the tutorial](#set-up-a-domain-name). If you didn't set up a domain name for +Again, remember that your Azure VM domain name will be the one you +[set up previously in the tutorial](#set-up-a-domain-name). If you didn't set up a domain name for your VM, you can use the IP address in its place in the following command: ```bash @@ -357,7 +356,7 @@ Provide your password at the prompt to authenticate. #### SSH from Windows (PuTTY) -If you're using [PuTTY] in Windows as your [SSH] client, then you might want to take a quick +If you're using [PuTTY] in Windows as your [SSH] client, then you might want to take a quick read on [using PuTTY in Windows][Using-SSH-In-Putty]. ### Updating GitLab @@ -369,8 +368,8 @@ version: sudo apt-get update && sudo apt-get install gitlab-ce ``` -This command will update GitLab and its associated components to the latest versions, so it will -take a little time to complete. You'll see various update tasks being completed in your SSH +This command will update GitLab and its associated components to the latest versions, so it will +take a little time to complete. You'll see various update tasks being completed in your SSH terminal window: ![GitLab updating](img/gitlab-ssh-update-in-progress.png) @@ -387,23 +386,23 @@ before anything else. #### Check out your updated GitLab -Refresh your GitLab instance in the browser and navigate to the Admin Area. You should now have an -up-to-date GitLab instance. +Refresh your GitLab instance in the browser and navigate to the Admin Area. You should now have an +up-to-date GitLab instance. -When we wrote this tutorial our Azure VM GitLab instance was updated to the latest version at time -of writing (`9.4.0`). You can see that the message which was previously displaying **"update asap"** +When we wrote this tutorial our Azure VM GitLab instance was updated to the latest version at time +of writing (`9.4.0`). You can see that the message which was previously displaying **"update asap"** is now showing **"up-to-date"**: ![GitLab up to date](img/gitlab-admin-area-9.4.0.png) ## Conclusion -Naturally, we believe that GitLab is a great git repository tool. However, GitLab is a whole lot -more than that too. GitLab unifies issues, code review, CI and CD into a single UI, helping you to -move faster from idea to production, and in this tutorial we showed you how quick and easy it is to -set up and run your own instance of GitLab on Azure, Microsoft's cloud service. +Naturally, we believe that GitLab is a great git repository tool. However, GitLab is a whole lot +more than that too. GitLab unifies issues, code review, CI and CD into a single UI, helping you to +move faster from idea to production, and in this tutorial we showed you how quick and easy it is to +set up and run your own instance of GitLab on Azure, Microsoft's cloud service. -Azure is a great way to experiment with GitLab, and if you decide (as we hope) that GitLab is for +Azure is a great way to experiment with GitLab, and if you decide (as we hope) that GitLab is for you, you can continue to use Azure as your secure, scalable cloud provider or of course run GitLab on any cloud service you choose. diff --git a/doc/install/installation.md b/doc/install/installation.md index a8064ae046e..fb24d4fa0ef 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -108,7 +108,7 @@ sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libs # Download and compile from source cd /tmp -curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz +curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz cd git-2.18.0/ ./configure diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md index 35024a78fca..4f7be70baf2 100644 --- a/doc/integration/akismet.md +++ b/doc/integration/akismet.md @@ -34,7 +34,6 @@ To use Akismet: ![Screenshot of Akismet settings](img/akismet_settings.png) - ## Training > *Note:* Training the Akismet filter is only available in 8.11 and above. diff --git a/doc/integration/cas.md b/doc/integration/cas.md index f757edf0bc2..c6178fa44f0 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -38,7 +38,6 @@ To enable the CAS OmniAuth provider you must register your application with your } ] ``` - For installations from source: @@ -65,4 +64,3 @@ On the sign in page there should now be a CAS tab in the sign in form. [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source - diff --git a/doc/integration/github.md b/doc/integration/github.md index eca9aa16499..bee68688ace 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -21,10 +21,10 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe - Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - Homepage URL: the URL to your GitLab installation. e.g., `https://gitlab.company.com` - Application description: Fill this in if you wish. - - Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth`. Please make sure the port is included if your GitLab instance is not configured on default port. + - Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth/github/callback`. Please make sure the port is included if your GitLab instance is not configured on default port. ![Register OAuth App](img/github_register_app.png) - NOTE: Be sure to append `/users/auth` to the end of the callback URL + NOTE: Be sure to append `/users/auth/github/callback` to the end of the callback URL to prevent a [OAuth2 convert redirect](http://tetraph.com/covert_redirect/) vulnerability. @@ -93,7 +93,6 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe args: { scope: 'user:email' } } ``` - For GitHub Enterprise: ``` diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index f5f1f9486c2..4db986197f3 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -231,7 +231,6 @@ In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> ![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) - ## Disabling Omniauth Starting from version 11.4 of GitLab, Omniauth is enabled by default. This only diff --git a/doc/integration/saml.md b/doc/integration/saml.md index bb3cd9a005f..8ee07a7fcdc 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -249,7 +249,6 @@ If you want some SAML authentication methods to count as 2FA on a per session ba 1. Save the file and [restart GitLab][] for the changes ot take effect - In addition to the changes in GitLab, make sure that your Idp is returning the `AuthnContext`. For example: diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 7d73026a6c6..82ba1d7d984 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -8,7 +8,6 @@ We suggest you use the project name as the trigger term for simplicity and clari Taking the trigger term as `project-name`, the commands are: - | Command | Effect | | ------- | ------ | | `/project-name help` | Shows all available slash commands | diff --git a/doc/intro/README.md b/doc/intro/README.md index d9acc5bdeac..9bc7c3d9ea4 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -9,13 +9,13 @@ comments: false Create projects and groups. - [Create a new project](../gitlab-basics/create-project.md) -- [Create a new group](../gitlab-basics/create-group.md) +- [Create a new group](../user/group/index.md#create-a-new-group) ## Prioritize Create issues, labels, milestones, cast your vote, and review issues. -- [Create a new issue](../user/project/issues/index.md#new-issue) +- [Create an issue](../user/project/issues/create_new_issue.md) - [Assign labels to issues](../user/project/labels.md) - [Use milestones as an overview of your project's tracker](../user/project/milestones/index.md) - [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 037b71a27b9..2514ca94775 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -442,7 +442,6 @@ backups will be copied to, and will be created if it does not exist. If the directory that you want to copy the tarballs to is the root of your mounted directory, just use `.` instead. - For Omnibus GitLab packages: 1. Edit `/etc/gitlab/gitlab.rb`: @@ -564,7 +563,6 @@ For installations from source: 1. [Restart GitLab] for the changes to take effect. - ```sh sudo -u git crontab -e # Edit the crontab for the git user ``` @@ -806,9 +804,22 @@ If you have failed to [back up the secrets file](#storing-configuration-files), then users with 2FA enabled will not be able to log into GitLab. In that case, you need to [disable 2FA for everyone](../security/two_factor_authentication.md#disabling-2fa-for-everyone). -In the case of CI/CD, if your project has secure variables set, you might experience -some weird behavior, like stuck jobs or 500 errors. In that case, you can try -deleting the `ci_variables` table from the database. +The secrets file is also responsible for storing the encryption key for several +columns containing sensitive information. If the key is lost, GitLab will be +unable to decrypt those columns. This will break a wide range of functionality, +including (but not restricted to): + +* [CI/CD variables](../ci/variables/README.md) +* [Kubernetes / GCP integration](../user/project/clusters/index.md) +* [Custom Pages domains](../user/project/pages/getting_started_part_three.md) +* [Project error tracking](../user/project/operations/error_tracking.md) +* [Runner authentication](../ci/runners/README.md) +* [Project mirroring](../workflow/repository_mirroring.md) +* [Web hooks](../user/project/integrations/webhooks.md) + +In the case of CI/CD, variables, you might experience some weird behavior, like +stuck jobs or 500 errors. In that case, you can try removing contents of the +`ci_group_variables` and `ci_project_variables` tables from the database. CAUTION: **Warning:** Use the following commands at your own risk, and make sure you've taken a @@ -828,9 +839,10 @@ backup beforehand. sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production ``` -1. Check the `ci_variables` table: +1. Check the `ci_group_variables` and `ci_variables` tables: ```sql + SELECT * FROM public."ci_group_variables"; SELECT * FROM public."ci_variables"; ``` @@ -839,6 +851,7 @@ backup beforehand. 1. Drop the table: ```sql + DELETE FROM ci_group_variables; DELETE FROM ci_variables; ``` @@ -848,5 +861,9 @@ backup beforehand. You should now be able to visit your project, and the jobs will start running again. +A similar strategy can be employed for the remaining features - by removing the +data that cannot be decrypted, GitLab can be brought back into working order, +and the lost data can be manually replaced. + [reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md index e1b1912ed47..571e784e530 100644 --- a/doc/raketasks/user_management.md +++ b/doc/raketasks/user_management.md @@ -52,7 +52,6 @@ bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production - Enable this setting to keep new users blocked until they have been cleared by the admin (default: false). - ``` block_auto_created_users: false ``` @@ -77,7 +76,6 @@ GitLab stores the secret data enabling 2FA to work in an encrypted database column. The encryption key for this data is known as `otp_key_base`, and is stored in `config/secrets.yml`. - If that file is leaked, but the individual 2FA secrets have not, it's possible to re-encrypt those secrets with a new encryption key. This allows you to change the leaked key without forcing all users to change their 2FA details. diff --git a/doc/security/README.md b/doc/security/README.md index e22dc00759d..a90127e0356 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -15,3 +15,4 @@ comments: false - [How we manage the CRIME vulnerability](crime_vulnerability.md) - [Enforce Two-factor authentication](two_factor_authentication.md) - [Send email confirmation on sign-up](user_email_confirmation.md) +- [Security of running jobs](https://docs.gitlab.com/runner/security/) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 1b53be15b44..9c4a391e8da 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -247,6 +247,17 @@ Public SSH keys need to be unique to GitLab, as they will bind to your account. Your SSH key is the only identifier you'll have when pushing code via SSH, that's why it needs to uniquely map to a single user. +## Per-repository SSH keys + +If you want to use different keys depending on the repository you are working +on, you can issue the following command while inside your repository: + +```sh +git config core.sshCommand "ssh -o IdentitiesOnly=yes -i ~/.ssh/private-key-filename-for-this-repository -F /dev/null" +``` + +This will not use the SSH Agent and requires at least Git 2.10. + ## Deploy keys ### Per-repository deploy keys diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 6d63906ea4d..55b678d6af5 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -138,7 +138,7 @@ Please refer to `group_rename` and `user_rename` for that case. "created_at": "2012-07-21T07:30:56Z", "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_add_to_team", - "project_access": "Maintainer", + "access_level": "Maintainer", "project_id": 74, "project_name": "StoreCloud", "project_path": "storecloud", diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index a354d3e7884..df6897002c9 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -21,7 +21,7 @@ This page gathers all the resources for the topic **Authentication** within GitL - [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa) - **Articles:** - [How to Configure LDAP with GitLab CE](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md) - - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/) + - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.md) - [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/) - [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/support-engineering/ldap/debugging_ldap.html) - **Integrations:** diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 91be3e3d45d..04d3b5cb277 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -58,15 +58,15 @@ project in a simple and automatic way: 1. [Auto Build](#auto-build) 1. [Auto Test](#auto-test) -1. [Auto Code Quality](#auto-code-quality) **[STARTER]** -1. [Auto SAST (Static Application Security Testing)](#auto-sast) **[ULTIMATE]** -1. [Auto Dependency Scanning](#auto-dependency-scanning) **[ULTIMATE]** -1. [Auto License Management](#auto-license-management) **[ULTIMATE]** +1. [Auto Code Quality](#auto-code-quality-starter) **[STARTER]** +1. [Auto SAST (Static Application Security Testing)](#auto-sast-ultimate) **[ULTIMATE]** +1. [Auto Dependency Scanning](#auto-dependency-scanning-ultimate) **[ULTIMATE]** +1. [Auto License Management](#auto-license-management-ultimate) **[ULTIMATE]** 1. [Auto Container Scanning](#auto-container-scanning) 1. [Auto Review Apps](#auto-review-apps) -1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) **[ULTIMATE]** +1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast-ultimate) **[ULTIMATE]** 1. [Auto Deploy](#auto-deploy) -1. [Auto Browser Performance Testing](#auto-browser-performance-testing) **[PREMIUM]** +1. [Auto Browser Performance Testing](#auto-browser-performance-testing-premium) **[PREMIUM]** 1. [Auto Monitoring](#auto-monitoring) As Auto DevOps relies on many different components, it's good to have a basic @@ -553,7 +553,7 @@ Herokuish](https://github.com/gliderlabs/herokuish#paths) > [Introduced][ce-19507] in GitLab 11.0. -For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token) +For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md#gitlab-deploy-token) will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token can be used for permanent access to the registry. diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md index 7195b0f0f04..8a8021dc36d 100644 --- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md +++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md @@ -45,7 +45,6 @@ Here's what we'll cover in this tutorial: - [How to modify history](#how-modifying-history-is-done) - [How to remove sensitive information from repository](#deleting-sensitive-information-from-commits) - ### Branching strategy [Git][git-official] is a de-centralized version control system, which means that beside regular @@ -64,14 +63,12 @@ prevent that anything is lost or out of sync when feature is complete. You can a read through this blog post on [Git Tips & Tricks][gitlab-git-tips-n-tricks] to learn how to easily **do** things in Git. - ## Undo local changes Until you push your changes to any remote repository, they will only affect you. That broadens your options on how to handle undoing them. Still, local changes can be on various stages and each stage has a different approach on how to tackle them. - ### Unstaged local changes (before you commit) When a change is made, but it is not added to the staged tree, Git itself @@ -315,7 +312,6 @@ In case you want to modify something introduced in commit `B`. You can find some more examples in [below section where we explain how to modify history](#how-modifying-history-is-done) - ### Redoing the Undo Sometimes you realize that the changes you undid were useful and you want them @@ -396,8 +392,8 @@ a nicer history of your contribution. Keep in mind that this also removes the comments attached to certain commits in merge requests, so if you need to retain traceability in GitLab, then modifying history is not acceptable. -A feature-branch of a merge request is a public branch and might be used by -other developers, but project process and rules might allow or require +A feature-branch of a merge request is a public branch and might be used by +other developers, but project process and rules might allow or require you to use `git rebase` (command that changes history) to reduce number of displayed commits on target branch after reviews are done (for example GitLab). There is a `git merge --squash` command which does exactly that diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index cf148a424db..254e234a22c 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -706,4 +706,3 @@ Files that have been modified but are not committed. Check them by using the com ### YAML A human-readable data serialization [language](http://www.yaml.org/about.html) that takes concepts from programming languages such as C, Perl, and Python, and ideas from XML and the data format of electronic mail. - diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md index 77a1892b656..b4ba5b7a24e 100644 --- a/doc/university/high-availability/aws/README.md +++ b/doc/university/high-availability/aws/README.md @@ -80,7 +80,6 @@ our newly created VPC. ![Route Table](img/route_table.png) - ### Internet Gateway Now still on the same dashboard head over to Internet Gateways and @@ -238,7 +237,6 @@ running reconfigure we need to make sure all our services are tied down so just leave the reconfigure command until after we edit our gitlab.rb file. - ### Extension for PostgreSQL Connect to your new RDS instance to verify access and to install @@ -277,8 +275,6 @@ username, and password. Next, we only need to configure the Redis section by adding the host and uncommenting the port. - - The last configuration step is to [change the default file locations ](http://docs.gitlab.com/ee/administration/high_availability/nfs.html) to make the EFS integration easier to manage. @@ -344,7 +340,6 @@ text area that allows us to add a lot of custom configurations which allows you to add a custom script for when launching an instance. Let's add the following script to the User Data section: - #cloud-config package_upgrade: true packages: diff --git a/doc/university/support/README.md b/doc/university/support/README.md index 805af253367..feb90ae9bad 100644 --- a/doc/university/support/README.md +++ b/doc/university/support/README.md @@ -2,7 +2,6 @@ comments: false --- - # Support Boot Camp **Goal:** Prepare new Service Engineers at GitLab diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md index 53578a34d1c..60c0eadc572 100644 --- a/doc/university/training/end-user/README.md +++ b/doc/university/training/end-user/README.md @@ -2,7 +2,6 @@ comments: false --- - # Training This training material is the markdown used to generate training slides @@ -205,7 +204,6 @@ git push origin squash_some_bugs - Anyone can comment, not just the assignee - Push corrections to the same branch - --- ### Merge request example @@ -395,7 +393,6 @@ git revert vs git reset Reset removes the commit while revert removes the changes but leaves the commit Revert is safer considering we can revert a revert - # Changed file git commit -am "bug introduced" git revert HEAD diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md index 127fdf4d44a..763ef802a04 100644 --- a/doc/university/training/topics/git_log.md +++ b/doc/university/training/topics/git_log.md @@ -40,7 +40,6 @@ Git log lists commit history. It allows searching and filtering. git log --since=1.month.ago --until=3.weeks.ago ``` - ---------- ## Git Log Workflow diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md index ca3a64e4347..96b89e3319a 100644 --- a/doc/university/training/topics/rollback_commits.md +++ b/doc/university/training/topics/rollback_commits.md @@ -51,7 +51,6 @@ comments: false 1. Pull for updates 1. Push changes - ---------- ## Commands diff --git a/doc/update/README.md b/doc/update/README.md index d098dc99518..e2fffadb1ea 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -5,15 +5,15 @@ update guides. There are currently 3 official ways to install GitLab: -- Omnibus packages -- Source installation -- Docker installation +- [Omnibus packages](#omnibus-packages) +- [Source installation](#installation-from-source) +- [Docker installation](#installation-using-docker) Based on your installation, choose a section below that fits your needs. ## Omnibus Packages -- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) +- The [Omnibus update guide][omni-update] contains the steps needed to update an Omnibus GitLab package. ## Installation from source @@ -36,7 +36,7 @@ can still be found in the Git repository: GitLab provides official Docker images for both Community and Enterprise editions. They are based on the Omnibus package and instructions on how to -update them are in [a separate document][omnidocker]. +update them are in [a separate document][omni-docker]. ## Upgrading without downtime @@ -103,6 +103,10 @@ migrations this could potentially lead to hours of downtime, depending on the size of your database. To work around this you will have to use PostgreSQL and meet the other online upgrade requirements mentioned above. +### Steps + +Steps to [upgrade without downtime][omni-zero-downtime]. + ## Upgrading between editions GitLab comes in two flavors: [Community Edition][ce] which is MIT licensed, @@ -150,3 +154,6 @@ possible. [ce]: https://about.gitlab.com/features/#community [ee]: https://about.gitlab.com/features/#enterprise [omni-ce-ee]: https://docs.gitlab.com/omnibus/update/README.html#updating-community-edition-to-enterprise-edition +[omni-docker]: https://docs.gitlab.com/omnibus/docker/README.html +[omni-update]: https://docs.gitlab.com/omnibus/update/README.html +[omni-zero-downtime]: https://docs.gitlab.com/omnibus/update/README.html#zero-downtime-updates diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index 7c6a14cb104..1f4b5fbffa7 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -97,7 +97,6 @@ Now, you can use pgloader to migrate the data from MySQL to PostgreSQL: 1. Once the migration finishes, you should see a summary table that looks like the following: - ``` table name read imported errors total time ----------------------------------------------- --------- --------- --------- -------------- @@ -245,7 +244,6 @@ Now, you can use pgloader to migrate the data from MySQL to PostgreSQL: 1. Once the migration finishes, you should see a summary table that looks like the following: - ``` table name read imported errors total time ----------------------------------------------- --------- --------- --------- -------------- @@ -286,4 +284,3 @@ If you experience 500 errors after the migration, try to clear the cache: ``` bash sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production ``` - diff --git a/doc/update/restore_after_failure.md b/doc/update/restore_after_failure.md index 01c52aae7f5..faa3a6e65cb 100644 --- a/doc/update/restore_after_failure.md +++ b/doc/update/restore_after_failure.md @@ -80,4 +80,3 @@ exit Once the migration is successfully marked, run the rake `db:migrate` task again. You will likely have to repeat this process several times until all failed migrations are marked complete. - diff --git a/doc/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md index d2e2cf439b5..835166837a8 100644 --- a/doc/update/upgrading_postgresql_using_slony.md +++ b/doc/update/upgrading_postgresql_using_slony.md @@ -270,7 +270,6 @@ sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 1 --conf /var/opt/gitlab If all went well this will produce output such as: - ``` Invoke slon for node 1 - /opt/gitlab/embedded/bin/slon -p /var/run/slony1/slony_replication_node1.pid -s 1000 -d2 slony_replication 'host=192.168.0.7 dbname=gitlabhq_production user=slony port=5432 password=hieng8ezohHuCeiqu0leeghai4aeyahp' > /var/log/gitlab/slony/node1/gitlabhq_production-2016-10-06.log 2>&1 & Slon successfully started for cluster slony_replication, node node1 diff --git a/doc/user/abuse_reports.md b/doc/user/abuse_reports.md index 1f4f598b121..41ee7e62b2c 100644 --- a/doc/user/abuse_reports.md +++ b/doc/user/abuse_reports.md @@ -25,7 +25,6 @@ To report abuse from a user's comment: 1. Complete an abuse report. 1. Click the **Send report** button. - NOTE: **Note:** A URL to the reported user's comment will be pre-filled in the abuse report's **Message** field. diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md index 43b1190fb48..c22982ac190 100644 --- a/doc/user/admin_area/monitoring/health_check.md +++ b/doc/user/admin_area/monitoring/health_check.md @@ -28,7 +28,6 @@ With default whitelist settings, the probes can be accessed from localhost: - `http://localhost/-/readiness` - `http://localhost/-/liveness` - The first endpoint, `/-/health/`, only checks whether the application server is running. It does -not verify the database or other services are running. A successful response will return a 200 status code with the following message: diff --git a/doc/user/admin_area/settings/third_party_offers.md b/doc/user/admin_area/settings/third_party_offers.md index 177251b52b9..23311801790 100644 --- a/doc/user/admin_area/settings/third_party_offers.md +++ b/doc/user/admin_area/settings/third_party_offers.md @@ -3,4 +3,7 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20379) > in [GitLab Core](https://about.gitlab.com/pricing/) 11.1 -The display of third party offers can be controlled in the Admin Area -> Settings page. +Within GitLab, we inform users of available third-party offers they might find valuable in order to enhance the development of their projects. +An example is the Google Cloud Platform free credit for using [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/). + +The display of third-party offers can be toggled in the Admin area on the Settings page. diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 84f4b0b3922..d202b1260ad 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -253,8 +253,9 @@ to newer issues or merge requests. - The people participating in the discussion are trolling, abusive, or otherwise being unproductive. -In these cases, a user with Maintainer permissions or higher in the project can lock (and unlock) -an issue or a merge request, using the "Lock" section in the sidebar: +In these cases, a user with Developer permissions or higher in the project can lock (and unlock) +an issue or a merge request, using the "Lock" section in the sidebar. For issues, +a user with Reporter permissions can lock (and unlock). | Unlock | Lock | | :-----------: | :----------: | diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 9fc50741407..8d223d1c5f0 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -95,7 +95,6 @@ For example, let's say we have the following Kubernetes clusters: | Test | `test` | Group | | Development| `*` | Group | - And the following environments are set in [`.gitlab-ci.yml`](../../../ci/yaml/README.md): ```yaml diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 9a01625f3ff..a7a87773eec 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -288,7 +288,6 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. ``` - Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px">. Well we have a gift for you: <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/zap.png" width="20px" height="20px">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/v.png" width="20px" height="20px"> @@ -453,14 +452,14 @@ Color written inside backticks will be followed by a color "chip". Examples: - `#F00` - `#F00A` - `#FF0000` - `#FF0000AA` - `RGB(0,255,0)` - `RGB(0%,100%,0%)` - `RGBA(0,255,0,0.7)` - `HSL(540,70%,50%)` + `#F00` + `#F00A` + `#FF0000` + `#FF0000AA` + `RGB(0,255,0)` + `RGB(0%,100%,0%)` + `RGBA(0,255,0,0.7)` + `HSL(540,70%,50%)` `HSLA(540,70%,50%,0.7)` Becomes: @@ -987,7 +986,6 @@ while the equation for the theory of relativity is E = mc<sup>2</sup>. The formula for water is H<sub>2</sub>O while the equation for the theory of relativity is E = mc<sup>2</sup>. - ## Wiki-specific Markdown The following examples show how links inside wikis behave. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 4976284aea4..74a966f3a17 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -61,6 +61,7 @@ The following table depicts the various user permission levels in a project. | Manage related issues **[STARTER]** | | ✓ | ✓ | ✓ | ✓ | | Lock issue discussions | | ✓ | ✓ | ✓ | ✓ | | Create issue from vulnerability **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ | +| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ | | Lock merge request discussions | | | ✓ | ✓ | ✓ | | Create new environments | | | ✓ | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ | @@ -101,6 +102,7 @@ The following table depicts the various user permission levels in a project. | Manage clusters | | | | ✓ | ✓ | | Manage license policy **[ULTIMATE]** | | | | ✓ | ✓ | | Edit comments (posted by any user) | | | | ✓ | ✓ | +| Manage Error Tracking | | | | ✓ | ✓ | | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 83bc79925e1..0a7c5832a2d 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -18,7 +18,7 @@ the second factor of authentication. Once enabled, in addition to supplying your password to login, you'll be prompted to activate your U2F device (usually by pressing a button on it), and it will perform secure authentication on your behalf. -The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend +The U2F workflow is only [supported by](https://caniuse.com/#search=U2F) Google Chrome, Opera and Firefox at this point, so we _strongly_ recommend that you set up both methods of two-factor authentication, so you can still access your account from other browsers. diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index efb031b8239..ba756c4b793 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -89,7 +89,6 @@ To enable private profile: 1. Check the "Private profile" option. 1. Hit **Update profile settings**. - NOTE: **Note:** You and GitLab admins can see your the abovementioned information on your profile even if it is private. diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 363d3db8db1..7387d1810ca 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -57,7 +57,10 @@ and default views of your dashboard and the projects' landing pages. ### Layout width GitLab can be set up to use different widths depending on your liking. Choose -between the fixed (max. 1200px) and the fluid (100%) application layout. +between the fixed (max. `1280px`) and the fluid (`100%`) application layout. + +NOTE: **Note:** +While `1280px` is the standard max width when using fixed layout, some pages still use 100% width, depending on the content. ### Default dashboard diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md index 0e6d4bce153..ea09f9f1547 100644 --- a/doc/user/project/clusters/eks_and_gitlab/index.md +++ b/doc/user/project/clusters/eks_and_gitlab/index.md @@ -253,7 +253,7 @@ With RBAC disabled and services deployed, [Auto DevOps](../../../../topics/autodevops/index.md) can now be leveraged to build, test, and deploy the app. -[Enable Auto DevOps](../../../../topics/autodevops/index.md##enablingdisabling-auto-devops-at-the-project-level) +[Enable Auto DevOps](../../../../topics/autodevops/index.md#enablingdisabling-auto-devops-at-the-project-level) if not already enabled. If a wildcard DNS entry was created resolving to the Load Balancer, enter it in the `domain` field under the Auto DevOps settings. Otherwise, the deployed app will not be externally available outside of the cluster. diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index aa1e165e3a2..fc3a4a757d0 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -138,7 +138,6 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ FUNCTION: echo ``` - The `serverless.yml` file references both an `echo` directory (under `buildargs`) and an `echo` file (under `handler`), which is a reference to `echo.js` in the [repository](https://gitlab.com/knative-examples/functions). Additionally, it contains three sections with distinct parameters: @@ -150,7 +149,6 @@ contains three sections with distinct parameters: | `service` | Name for the Knative service which will serve the function. | | `description` | A short description of the `service`. | - ### `provider` | Parameter | Description | diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 638b73bfcb6..dec6eac2508 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -47,7 +47,6 @@ to enable it. > - To move or rename a repository with a container registry you will have to > delete all existing images. - If you visit the **Registry** link under your project's menu, you can see the explicit instructions to login to the Container Registry using your GitLab credentials. diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index b98221bea61..529a82ded9e 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -46,7 +46,7 @@ You can see that there are seven stages in total: ## How the data is measured Cycle Analytics records cycle time and data based on the project issues with the -exception of the staging and production stages, where only data deployed to +exception of the staging and production stages, where only data deployed to production are measured. Specifically, if your CI is not set up and you have not defined a `production` @@ -86,7 +86,6 @@ So, the Cycle Analytics dashboard won't present any data: - For staging and production stages, if the project has no `production` or `production/*` environment. - ## Example workflow Below is a simple fictional workflow of a single cycle that happens in a @@ -159,7 +158,6 @@ Learn more about Cycle Analytics in the following resources: - [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/) - [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/) - [board]: issue_board.md#creating-a-new-list [ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986 [ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975 diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md index 7c94f4ef4d8..08647caaf07 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -93,4 +93,3 @@ Possible fixes [ce-4981]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4981 [gitlab-ce-templates]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/.gitlab - diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md index 48aabd02438..3206a39dc08 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -59,4 +59,3 @@ Bamboo under 'Trigger IP addresses'. > **Note:** > - Starting with GitLab 8.14.0, builds are triggered on push events. - diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index a4698fd172a..ed289b0c4eb 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -89,7 +89,7 @@ to integrate with. Once configured, GitLab will attempt to retrieve performance metrics for any environment which has had a successful deployment. -GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metrics Library documentation](prometheus_library/index.md). +GitLab will automatically scan the Prometheus server for metrics from known servers like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metrics Library documentation](prometheus_library/index.md). You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments). diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 4d1d95da6f0..cbf08a4f30a 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -22,12 +22,37 @@ a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the webhook URL. +In most cases, you'll need to set up your own [webhook receiver](#example-webhook-receiver) +to receive information from GitLab, and send it to another app, according to your needs. +We already have a [built-in receiver](http://docs.gitlab.com/ce/project_services/slack.html) +for sending [Slack](https://api.slack.com/incoming-webhooks) notifications _per project_. + +## Overview + +[Webhooks](https://en.wikipedia.org/wiki/Webhook) are "_user-defined HTTP +callbacks_". They are usually triggered by some +event, such as pushing code to a repository or a comment being posted to a blog. +When that event occurs, the source app makes an HTTP request to the URI +configured for the webhook. The action taken may be anything. +Common uses are to trigger builds with continuous integration systems or to +notify bug tracking systems. + Webhooks can be used to update an external issue tracker, trigger CI jobs, update a backup mirror, or even deploy to your production server. +They are available **per project** for GitLab Community Edition, +and **per project and per group** for **GitLab Enterprise Edition**. Navigate to the webhooks page by going to your project's **Settings ➔ Integrations**. +## Use-cases + +- You can set up a webhook in GitLab to send a notification to +[Slack](https://api.slack.com/incoming-webhooks) every time a build fails, for example +- You can [integrate with Twilio to be notified via SMS](https://www.datadoghq.com/blog/send-alerts-sms-customizable-webhooks-twilio/) +every time an issue is created for a specific project or group within GitLab +- You can use them to [automatically assign labels to merge requests](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/). + ## Webhook endpoint tips If you are writing your own endpoint (web server) that will receive @@ -1203,6 +1228,15 @@ by uncommenting or adding the following setting to your `/etc/gitlab/gitlab.rb`: gitlab_rails['webhook_timeout'] = 10 ``` +### Troubleshooting: "Unable to get local issuer certificate" + +When SSL verification is enabled, this error indicates that GitLab isn't able to verify the SSL certificate of the webhook endpoint. +Typically, this is because the root certificate isn't issued by a trusted certification authority as +determined by [CAcert.org](http://www.cacert.org/). + +Should that not be the case, consider using [SSL Checker](https://www.sslshopper.com/ssl-checker.html) to identify faults. +Missing intermediate certificates are a common point of verification failure. + ## Example webhook receiver If you want to see GitLab's webhooks in action for testing purposes you can use diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md index d78721f8658..e4a3ff52e07 100644 --- a/doc/user/project/issues/issues_functionalities.md +++ b/doc/user/project/issues/issues_functionalities.md @@ -4,7 +4,7 @@ Please read through the [GitLab Issue Documentation](index.md) for an overview o ## Issues Functionalities -The image bellow illustrates how an issue looks like: +The image below illustrates what an issue looks like: ![Issue view](img/issues_main_view_numbered.jpg) @@ -13,7 +13,7 @@ You can find all the information on that issue on one screen. ### Issue screen An issue starts with its status (open or closed), followed by its author, -and includes many other functionalities, numbered on the image above to +and includes many other functionalities, numbered in the image above to explain what they mean, one by one. Many of the elements of the issue screen refresh automatically, such as the title and description, when they are changed by another user. @@ -27,7 +27,7 @@ Comments and system notes also appear automatically in response to various actio #### 2. Todos -- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list +- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.md) list - Mark todo as done: mark that issue as done (reflects on the Todo list) #### 3. Assignee @@ -43,14 +43,14 @@ assigned to them if they created the issue themselves. ##### 3.1. Multiple Assignees **[STARTER]** -Often multiple people likely work on the same issue together, -which can especially be difficult to track in large teams +Often multiple people work on the same issue together, +which can be especially difficult to track in large teams where there is shared ownership of an issue. -In [GitLab Starter](https://about.gitlab.com/pricing/), you can also -select multiple assignees to an issue. +In [GitLab Starter](https://about.gitlab.com/pricing/), you can +assign multiple people to an issue. -Learn more on the [Multiple Assignees documentation](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html). +Learn more in the [Multiple Assignees documentation](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html). #### 4. Milestone @@ -58,40 +58,39 @@ Learn more on the [Multiple Assignees documentation](https://docs.gitlab.com/ee/ #### 5. Time Tracking -- Estimate time: add an estimate time in which the issue will be implemented -- Spend: add the time spent on the implementation of that issue +- Estimate time: add an estimate of the time it will take to resolve the issue. +- Spend: add the time spent on the resolution of the issue > **Note:** Both estimate and spend times are set via [GitLab Quick Actions](../quick_actions.md). -Learn more on the [Time Tracking documentation](../../../workflow/time_tracking.md). +Learn more in the [Time Tracking documentation](../../../workflow/time_tracking.md). #### 6. Due date -When you work on a tight schedule, and it's important to -have a way to set up a deadline for implementations and for solving -problems. This can be facilitated by the [due date](due_dates.md)). Due dates +When you work on a tight schedule, it's important to +have a way to set a deadline for implementations and for solving +problems. This can be done in the [due date](due_dates.md) element. Due dates can be changed as many times as needed. #### 7. Labels Categorize issues by giving them [labels](../labels.md). They help to -organize team's workflows, once they enable you to work with the -[GitLab Issue Board](index.md#gitlab-issue-board). +organize workflows, and they enable you to work with the +[GitLab Issue Board](index.md#issue-board). -Group Labels, which allow you to use the same labels per +Group Labels, which allow you to use the same labels for a group of projects, can be also given to issues. They work exactly the same, but they are immediately available to all projects in the group. > **Tip:** -if the label doesn't exist yet, when you click **Edit**, it opens a dropdown menu from which you can select **Create new label**. +If a label doesn't exist yet, you can click **Edit**, and it opens a dropdown menu from which you can select **Create new label**. #### 8. Weight **[STARTER]** -- Attribute a weight (in a 0 to 9 range) to that issue. Easy to complete -should weight 1 and very hard to complete should weight 9. +- Assign a weight. Larger values are used to indicate more effort is required to complete the issue. Only positive values or zero are allowed. -Learn more on the [Issue Weight documentation](https://docs.gitlab.com/ee/workflow/issue_weight.html). +Learn more in the [Issue Weight documentation](https://docs.gitlab.com/ee/workflow/issue_weight.html). #### 9. Participants @@ -102,32 +101,33 @@ Learn more on the [Issue Weight documentation](https://docs.gitlab.com/ee/workfl - Subscribe: if you are not a participant of the discussion on that issue, but want to receive notifications on each new input, subscribe to it. - Unsubscribe: if you are receiving notifications on that issue but no -longer want to receive them, unsubscribe to it. +longer want to receive them, unsubscribe from it. -Read more on the [notifications documentation](../../../workflow/notifications.md#issue-merge-request-events). +Read more in the [notifications documentation](../../../workflow/notifications.md#issue--merge-request-events). #### 11. Reference -- A quick "copy to clipboard" button to that issue's reference, `foo/bar#xxx`, where `foo` is the `username` or `groupname`, `bar` +- A quick "copy to clipboard" button for that issue's reference, `foo/bar#xxx`, where `foo` is the `username` or `groupname`, `bar` is the `project-name`, and `xxx` is the issue number. #### 12. Title and description -- Title: a plain text title describing the issue's subject. -- Description: a text field which fully supports [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm). +- Title: a plain text title for describing the subject of the issue. +- Description: a large text field which fully supports [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm), + to describe all the details of the issue. -#### 13. @mentions +#### 13. Mentions -- You can either `@mention` a user or a group present in your - GitLab instance and they will be notified via todos and email, unless that - person has disabled all notifications in their profile settings. -- Mentions for yourself (the current logged in user),will be distinctly highlighted +- You can mention a user or a group present in your GitLab instance with + `@username` or `@groupname` and they will be notified via todos and email, unless + they have disabled all notifications in their profile settings. +- Mentions for yourself (the current logged in user), will be highlighted in a different color, allowing you to easily see which comments involve you, helping you focus on them quickly. -To change your [notification settings](../../../workflow/notifications.md) navigate to +To change your [notification settings](../../../workflow/notifications.md), navigate to **Profile Settings** > **Notifications** > **Global notification level** -and choose your preferences from the dropdown menu. +and choose your preference from the dropdown menu. > **Tip:** Avoid mentioning `@all` in issues and merge requests, @@ -138,14 +138,14 @@ interpreted as spam. #### 14. Related Merge Requests - Any merge requests mentioned in that issue's description -or in the issue thread. +or in the issue discussion thread. #### 15. Award emoji - Award an emoji to that issue. > **Tip:** -Posting "+1" as comments in threads spam all +Posting "+1" as a comment in a thread spams all subscribed participants of that issue. Awarding an emoji is a way to let them know you like it without spamming them. @@ -157,7 +157,7 @@ These text fields also fully support #### 17. Comment, start a discussion, or comment and close -Once you wrote your comment, you can either: +Once you write a comment, you can either: - Click "Comment" and your comment will be published. - Click "Start discussion": start a thread within that issue's thread to discuss specific points. @@ -167,6 +167,6 @@ Once you wrote your comment, you can either: - Create a new merge request (with a new source branch named after the issue) in one action. The merge request will automatically inherit the milestone and labels of the issue. The merge -request will automatically close that issue as soon as merged. +request will automatically close that issue when it is merged. - Optionally, you can just create a [new branch](../repository/web_editor.md#create-a-new-branch-from-an-issue) named after that issue. diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md index ac58a0b5c18..e6033ca8655 100644 --- a/doc/user/project/milestones/index.md +++ b/doc/user/project/milestones/index.md @@ -28,8 +28,9 @@ the milestone to the issue. ## Project milestones and group milestones -- **Project milestones** can be assigned to issues or merge requests in that project only. -- **Group milestones** can be assigned to any issue or merge request of any project in that group. +- **Project milestones** can be assigned to issues or merge requests in that project only. Navigate to **Issues > Milestones** in a project to view the project milestone list. +- **Group milestones** can be assigned to any issue or merge request of any project in that group. Navigate to **Issues > Milestones** in a group to view the group milestone list. +- All milestones you have access to can also be viewed in the dashboard milestones list. Click on **Milestones** on the top navigation bar to view both project milestones and group milestones you have access to. ## Creating milestones diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md index fe4b36062f7..90bb92d2062 100644 --- a/doc/user/project/operations/error_tracking.md +++ b/doc/user/project/operations/error_tracking.md @@ -14,10 +14,14 @@ You may sign up to the cloud hosted <https://sentry.io> or deploy your own [on-p ### Enabling Sentry +NOTE: **Note:** +You will need at least Maintainer [permissions](../../permissions.md) to enable the Sentry integration. + GitLab provides an easy way to connect Sentry to your project: 1. Sign up to Sentry.io or [deploy your own](#deploying-sentry) Sentry instance. 1. [Find or generate](https://docs.sentry.io/api/auth/) a Sentry auth token for your Sentry project. +Make sure to give the token at least the following scopes: `event:read` and `project:read`. 1. Navigate to your project’s **Settings > Operations** and provide the Sentry API URL and auth token. 1. Ensure that the 'Active' checkbox is set. 1. Click **Save changes** for the changes to take effect. @@ -25,6 +29,9 @@ GitLab provides an easy way to connect Sentry to your project: ## Error Tracking List +NOTE: **Note:** +You will need at least Reporter [permissions](../../permissions.md) to view the Error Tracking list. + The Error Tracking list may be found at **Operations > Error Tracking** in your project's sidebar. ![Error Tracking list](img/error_tracking_list.png) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 11f6165fcb4..2de3fb7e080 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -66,7 +66,7 @@ publish any website written directly in plain HTML, CSS, and JavaScript.</p> If you're using GitLab.com, your website will be publicly available to the internet. If you're using self-managed instances (Core, Starter, Premium, or Ultimate), your websites will be published on your own server, according to the -[Pages admin settings](../../../administration/pages/index.md) chosen by your sysdamin, +[Pages admin settings](../../../administration/pages/index.md) chosen by your sysadmin, who can opt for making them public or internal to your server. ### How it works diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index 0d0575b1ab4..4ab66063dcf 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -191,5 +191,9 @@ artifacts and the job's trace. 1. Click the trash icon at the top right of the job's trace. 1. Confirm the deletion. +## Retrieve artifacts of private projects when using GitLab CI + +In order to retrieve a job artifact of a different project, you might need to use a private token in order to [authenticate and download](../../../api/jobs.md#get-job-artifacts) the artifacts. + [expiry date]: ../../../ci/yaml/README.md#artifacts-expire_in -[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399 +[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399
\ No newline at end of file diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index bb9b4238ee9..cce330aecc7 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -23,6 +23,7 @@ in `.gitlab-ci.yml`. ## Timeout Timeout defines the maximum amount of time in minutes that a job is able run. +This is configureable under your project's **Settings > CI/CD > General pipelines settings**. The default value is 60 minutes. Decrease the time limit if you want to impose a hard limit on your jobs' running time or increase it otherwise. In any case, if the job surpasses the threshold, it is marked as failed. diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md index 3d8fff9f733..26bec323f02 100644 --- a/doc/user/project/protected_tags.md +++ b/doc/user/project/protected_tags.md @@ -10,7 +10,6 @@ This feature evolved out of [Protected Branches](protected_branches.md) Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Maintainer permission will be prevented from creating tags. - ## Configuring protected tags To protect a tag, you need to have at least Maintainer permission level. @@ -43,7 +42,6 @@ matching the wildcard. For example: | `*gitlab*` | `gitlab`, `gitlab/v1` | | `*` | `v1.0.1rc2`, `accidental-tag` | - Two different wildcards can potentially match the same tag. For example, `*-stable` and `production-*` would both match a `production-stable` tag. In that case, if _any_ of these protected tags have a setting like @@ -54,7 +52,6 @@ all matching tags: ![Protected tag matches](img/protected_tag_matches.png) - --- [ce-10356]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10356 "Protected Tags" diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 85a03d125dd..392e72dcc5c 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -55,7 +55,6 @@ discussions, and descriptions: | `/merge` | Merge (when pipeline succeeds) | | ✓ | | `/create_merge_request <branch name>` | Create a new merge request starting from the current issue | ✓ | | - ## Quick actions for commit messages The following quick actions are applicable for commit messages: diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md index c7e20f01a75..6495ede42e0 100644 --- a/doc/user/project/repository/gpg_signed_commits/index.md +++ b/doc/user/project/repository/gpg_signed_commits/index.md @@ -190,7 +190,6 @@ key to use. Replace `30F2B65B9246B6CA` with your GPG key ID. - 1. (Optional) If Git is using `gpg` and you get errors like `secret key not available` or `gpg: signing failed: secret key not available`, run the following command to change to `gpg2`: @@ -266,3 +265,7 @@ To remove a GPG key from your account: You can configure your project to reject commits that aren't GPG-signed via [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html). + +## GPG signing API + +Learn how to [get the GPG signature from a commit via API](../../../../api/commits.md#get-gpg-signature-of-a-commit). diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index 035028c9266..ce9d23bf911 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -33,13 +33,13 @@ easy for you. ![First file for your project](img/web_editor_template_dropdown_first_file.png) -When clicking on either `LICENSE` or `.gitignore`, a dropdown will be displayed +When clicking on either `LICENSE` or `.gitignore`, etc., a dropdown will be displayed to provide you with a template which might be suitable for your project. ![MIT license selected](img/web_editor_template_dropdown_mit_license.png) The license, changelog, contribution guide, or `.gitlab-ci.yml` file could also -be added through a button on the project page. In the example below the license +be added through a button on the project page. In the example below, the license has already been created, which creates a link to the license itself. ![New file button](img/web_editor_template_dropdown_buttons.png) @@ -112,7 +112,6 @@ screenshot above will yield a branch named Since GitLab 9.0, when you click the `New branch` in an empty repository project, GitLab automatically creates the master branch, commits a blank `README.md` file to it and creates and redirects you to a new branch based on the issue title. If your [project is already configured with a deployment service][project-services-doc] (e.g. Kubernetes), GitLab takes one step further and prompts you to set up [auto deploy][auto-deploy-doc] by helping you create a `.gitlab-ci.yml` file. - After the branch is created, you can edit files in the repository to fix the issue. When a merge request is created based on the newly created branch, the description field will automatically display the [issue closing pattern] diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index b09a3f927d1..d5f4a7fd4ac 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -121,7 +121,7 @@ NOTE: **Note:** GitLab administrators can use the admin interface to move any project to any namespace if needed. -[permissions]: ../../permissions.md##project-members-permissions +[permissions]: ../../permissions.md#project-members-permissions ## Operations settings diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 55e53b865af..3e85e97d7a5 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -3,8 +3,8 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4539) in [GitLab Ultimate][ee] 10.4. > [Brought to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/issues/44157) in 10.7. -The Web IDE makes it faster and easier to contribute changes to your projects -by providing an advanced editor with commit staging. +The Web IDE editor makes it faster and easier to contribute changes to your +projects by providing an advanced editor with commit staging. ## Open the Web IDE @@ -22,7 +22,7 @@ searching. The file finder is launched using the keyboard shortcut `Command-p`, `Control-p`, or `t` (when editor is not in focus). Type the filename or file path fragments to start seeing results. -## Syntax highligting +## Syntax highlighting As expected from an IDE, syntax highlighting for many languages within the Web IDE will make your direct editing even easier. @@ -47,10 +47,10 @@ Single file editing is based on the [Ace Editor](https://ace.c9.io). After making your changes, click the Commit button in the bottom left to review the list of changed files. Click on each file to review the changes and -click the tick icon to stage the file. +click the tick icon to stage the file. Once you have staged some changes, you can add a commit message and commit the -staged changes. Unstaged changes will not be commited. +staged changes. Unstaged changes will not be committed. ![Commit changes](img/commit_changes.png) @@ -80,7 +80,7 @@ left. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19318) in [GitLab Core][ce] 11.0. Switching between your authored and assigned merge requests can be done without -leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list +leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list of merge requests. You will need to commit or discard all your changes before switching to a different merge request. @@ -89,7 +89,7 @@ switching to a different merge request. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20850) in [GitLab Core][ce] 11.2. Switching between branches of the current project repository can be done without -leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list +leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list of branches. You will need to commit or discard all your changes before switching to a different branch. diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md index 45c306f5988..a14df6c8402 100644 --- a/doc/user/reserved_names.md +++ b/doc/user/reserved_names.md @@ -61,8 +61,6 @@ Currently the following names are reserved as top level groups: - favicon.ico - favicon.png - groups -- header_logo_dark.png -- header_logo_light.png - health_check - help - import diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md index 733d079bd4a..02be0ad191d 100644 --- a/doc/workflow/forking_workflow.md +++ b/doc/workflow/forking_workflow.md @@ -10,7 +10,6 @@ document more information about using branches to work together. Forking a project is in most cases a two-step process. - 1. Click on the fork button located in the middle of the page or a project's home page right next to the stars button. diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index a7313082fac..1b9fb504b15 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -2,314 +2,324 @@ # Introduction to GitLab Flow -Version management with git makes branching and merging much easier than older versioning systems such as SVN. -This allows a wide variety of branching strategies and workflows. -Almost all of these are an improvement over the methods used before git. -But many organizations end up with a workflow that is not clearly defined, overly complex or not integrated with issue tracking systems. -Therefore we propose the GitLab flow as clearly defined set of best practices. -It combines [feature driven development](https://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](http://martinfowler.com/bliki/FeatureBranch.html) with issue tracking. +Git allows a wide variety of branching strategies and workflows. +Because of this, many organizations end up with workflows that are too complicated, not clearly defined, or not integrated with issue tracking systems. +Therefore, we propose GitLab flow as a clearly defined set of best practices. +It combines [feature-driven development](https://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](https://martinfowler.com/bliki/FeatureBranch.html) with issue tracking. -Organizations coming to git from other version control systems frequently find it hard to develop an effective workflow. -This article describes the GitLab flow that integrates the git workflow with an issue tracking system. -It offers a simple, transparent and effective way to work with git. +Organizations coming to Git from other version control systems frequently find it hard to develop a productive workflow. +This article describes GitLab flow, which integrates the Git workflow with an issue tracking system. +It offers a simple, transparent, and effective way to work with Git. ![Four stages (working copy, index, local repo, remote repo) and three steps between them](four_stages.png) -When converting to git you have to get used to the fact that there are three steps before a commit is shared with colleagues. -Most version control systems have only one step, committing from the working copy to a shared server. -In git you add files from the working copy to the staging area. After that you commit them to the local repo. +When converting to Git, you have to get used to the fact that it takes three steps to share a commit with colleagues. +Most version control systems have only one step: committing from the working copy to a shared server. +In Git, you add files from the working copy to the staging area. After that, you commit them to your local repo. The third step is pushing to a shared remote repository. -After getting used to these three steps the branching model becomes the challenge. +After getting used to these three steps, the next challenge is the branching model. -![Multiple long running branches and merging in all directions](messy_flow.png) +![Multiple long-running branches and merging in all directions](messy_flow.png) -Since many organizations new to git have no conventions how to work with it, it can quickly become a mess. -The biggest problem they run into is that many long running branches that each contain part of the changes are around. -People have a hard time figuring out which branch they should develop on or deploy to production. -Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html). -We think there is still room for improvement and will detail a set of practices we call GitLab flow. +Since many organizations new to Git have no conventions for how to work with it, their repositories can quickly become messy. +The biggest problem is that many long-running branches emerge that all contain part of the changes. +People have a hard time figuring out which branch has the latest code, or which branch to deploy to production. +Frequently, the reaction to this problem is to adopt a standardized pattern such as [Git flow](https://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html). +We think there is still room for improvement. In this document, we describe a set of practices we call GitLab flow. ## Git flow and its problems ![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) -Git flow was one of the first proposals to use git branches and it has gotten a lot of attention. -It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes. -The development happens on the develop branch, moves to a release branch and is finally merged into the master branch. -Git flow is a well defined standard but its complexity introduces two problems. -The first problem is that developers must use the develop branch and not master, master is reserved for code that is released to production. -It is a convention to call your default branch master and to mostly branch from and merge to this. -Since most tools automatically make the master branch the default one and display that one by default it is annoying to have to switch to another one. -The second problem of git flow is the complexity introduced by the hotfix and release branches. +Git flow was one of the first proposals to use Git branches, and it has received a lot of attention. +It suggests a `master` branch and a separate `develop` branch, as well as supporting branches for features, releases, and hotfixes. +The development happens on the `develop` branch, moves to a release branch, and is finally merged into the `master` branch. + +Git flow is a well-defined standard, but its complexity introduces two problems. +The first problem is that developers must use the `develop` branch and not `master`. `master` is reserved for code that is released to production. +It is a convention to call your default branch `master` and to mostly branch from and merge to this. +Since most tools automatically use the `master` branch as the default, it is annoying to have to switch to another branch. + +The second problem of Git flow is the complexity introduced by the hotfix and release branches. These branches can be a good idea for some organizations but are overkill for the vast majority of them. -Nowadays most organizations practice continuous delivery which means that your default branch can be deployed. -This means that hotfix and release branches can be prevented including all the ceremony they introduce. +Nowadays, most organizations practice continuous delivery, which means that your default branch can be deployed. +Continuous delivery removes the need for hotfix and release branches, including all the ceremony they introduce. An example of this ceremony is the merging back of release branches. Though specialized tools do exist to solve this, they require documentation and add complexity. -Frequently developers make a mistake and for example changes are only merged into master and not into the develop branch. -The root cause of these errors is that git flow is too complex for most of the use cases. -And doing releases doesn't automatically mean also doing hotfixes. +Frequently, developers make mistakes such as merging changes only into `master` and not into the `develop` branch. +The reason for these errors is that Git flow is too complicated for most use cases. +For example, many projects do releases but don't need to do hotfixes. ## GitHub flow as a simpler alternative ![Master branch with feature branches merged in](github_flow.png) -In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). -This flow has only feature branches and a master branch. -This is very simple and clean, many organizations have adopted it with great success. -Atlassian recommends [a similar strategy](http://blogs.atlassian.com/2014/01/simple-git-workflow-simple/) although they rebase feature branches. -Merging everything into the master branch and deploying often means you minimize the amount of code in 'inventory' which is in line with the lean and continuous delivery best practices. -But this flow still leaves a lot of questions unanswered regarding deployments, environments, releases and integrations with issues. -With GitLab flow we offer additional guidance for these questions. +In reaction to Git flow, GitHub created a simpler alternative. +[GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `master` branch. +This flow is clean and straightforward, and many organizations have adopted it with great success. +Atlassian recommends [a similar strategy](https://www.atlassian.com/blog/archives/simple-git-workflow-simple), although they rebase feature branches. +Merging everything into the `master` branch and frequently deploying means you minimize the amount of unreleased code, which is in line with lean and continuous delivery best practices. +However, this flow still leaves a lot of questions unanswered regarding deployments, environments, releases, and integrations with issues. +With GitLab flow, we offer additional guidance for these questions. ## Production branch with GitLab flow -![Master branch and production branch with arrow that indicate deployments](production_branch.png) +![Master branch and production branch with an arrow that indicates a deployment](production_branch.png) -GitHub flow does assume you are able to deploy to production every time you merge a feature branch. -This is possible for e.g. SaaS applications, but there are many cases where this is not possible. -One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation. -Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times. -In these cases you can make a production branch that reflects the deployed code. -You can deploy a new version by merging in master to the production branch. -If you need to know what code is in production you can just checkout the production branch to see. +GitHub flow assumes you can deploy to production every time you merge a feature branch. +While this is possible in some cases, such as SaaS applications, there are many cases where this is not possible. +One case is where you don't control the timing of a release, for example, an iOS application that is released when it passes App Store validation. +Another case is when you have deployment windows — for example, workdays from 10 AM to 4 PM when the operations team is at full capacity — but you also merge code at other times. +In these cases, you can make a production branch that reflects the deployed code. +You can deploy a new version by merging `master` into the production branch. +If you need to know what code is in production, you can just checkout the production branch to see. The approximate time of deployment is easily visible as the merge commit in the version control system. This time is pretty accurate if you automatically deploy your production branch. -If you need a more exact time you can have your deployment script create a tag on each deployment. -This flow prevents the overhead of releasing, tagging and merging that is common to git flow. +If you need a more exact time, you can have your deployment script create a tag on each deployment. +This flow prevents the overhead of releasing, tagging, and merging that happens with Git flow. ## Environment branches with GitLab flow ![Multiple branches with the code cascading from one to another](environment_branches.png) -It might be a good idea to have an environment that is automatically updated to the master branch. -Only in this case, the name of this environment might differ from the branch name. -Suppose you have a staging environment, a pre-production environment and a production environment. -In this case the master branch is deployed on staging. When someone wants to deploy to pre-production they create a merge request from the master branch to the pre-production branch. -And going live with code happens by merging the pre-production branch into the production branch. -This workflow where commits only flow downstream ensures that everything has been tested on all environments. -If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch. -If master is good to go (it should be if you are practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches. -If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches. +It might be a good idea to have an environment that is automatically updated to the `master` branch. +Only, in this case, the name of this environment might differ from the branch name. +Suppose you have a staging environment, a pre-production environment, and a production environment. +In this case, deploy the `master` branch to staging. +To deploy to pre-production, create a merge request from the `master` branch to the pre-production branch. +Go live by merging the pre-production branch into the production branch. +This workflow, where commits only flow downstream, ensures that everything is tested in all environments. +If you need to cherry-pick a commit with a hotfix, it is common to develop it on a feature branch and merge it into `master` with a merge request. +In this case, do not delete the feature branch yet. +If `master` passes automatic testing, you then merge the feature branch into the other branches. +If this is not possible because more manual testing is required, you can send merge requests from the feature branch to the downstream branches. ## Release branches with GitLab flow ![Master and multiple release branches that vary in length with cherry-picks from master](release_branches.png) -Only in case you need to release software to the outside world you need to work with release branches. -In this case, each branch contains a minor version (2-3-stable, 2-4-stable, etc.). -The stable branch uses master as a starting point and is created as late as possible. -By branching as late as possible you minimize the time you have to apply bug fixes to multiple branches. -After a release branch is announced, only serious bug fixes are included in the release branch. -If possible these bug fixes are first merged into master and then cherry-picked into the release branch. -This way you can't forget to cherry-pick them into master and encounter the same bug on subsequent releases. -This is called an 'upstream first' policy that is also practiced by [Google](https://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](https://www.redhat.com/about/news/archive/2013/5/a-community-for-using-openstack-with-red-hat-rdo). -Every time a bug-fix is included in a release branch the patch version is raised (to comply with [Semantic Versioning](http://semver.org/)) by setting a new tag. +You only need to work with release branches if you need to release software to the outside world. +In this case, each branch contains a minor version, for example, 2-3-stable, 2-4-stable, etc. +Create stable branches using `master` as a starting point, and branch as late as possible. +By doing this, you minimize the length of time during which you have to apply bug fixes to multiple branches. +After announcing a release branch, only add serious bug fixes to the branch. +If possible, first merge these bug fixes into `master`, and then cherry-pick them into the release branch. +If you start by merging into the release branch, you might forget to cherry-pick them into `master`, and then you'd encounter the same bug in subsequent releases. +Merging into `master` and then cherry-picking into release is called an "upstream first" policy, which is also practiced by [Google](https://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](https://www.redhat.com/en/blog/a-community-for-using-openstack-with-red-hat-rdo). +Every time you include a bug fix in a release branch, increase the patch version (to comply with [Semantic Versioning](https://semver.org/)) by setting a new tag. Some projects also have a stable branch that points to the same commit as the latest released branch. -In this flow it is not common to have a production branch (or git flow master branch). +In this flow, it is not common to have a production branch (or Git flow `master` branch). ## Merge/pull requests with GitLab flow -![Merge request with line comments](mr_inline_comments.png) +![Merge request with inline comments](mr_inline_comments.png) -Merge or pull requests are created in a git management application and ask an assigned person to merge two branches. -Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch. -Tools such as GitLab and others choose the name merge request since that is the final action that is requested of the assignee. -In this article we'll refer to them as merge requests. +Merge or pull requests are created in a Git management application. They ask an assigned person to merge two branches. +Tools such as GitHub and Bitbucket choose the name "pull request" since the first manual action is to pull the feature branch. +Tools such as GitLab and others choose the name "merge request" since the final action is to merge the feature branch. +In this article, we'll refer to them as merge requests. -If you work on a feature branch for more than a few hours it is good to share the intermediate result with the rest of the team. -This can be done by creating a merge request without assigning it to anyone, instead you mention people in the description or a comment (/cc @mark @susan). -This means it is not ready to be merged but feedback is welcome. +If you work on a feature branch for more than a few hours, it is good to share the intermediate result with the rest of the team. +To do this, create a merge request without assigning it to anyone. +Instead, mention people in the description or a comment, for example, "/cc @mark @susan." +This indicates that the merge request is not ready to be merged yet, but feedback is welcome. Your team members can comment on the merge request in general or on specific lines with line comments. -The merge requests serves as a code review tool and no separate tools such as Gerrit and reviewboard should be needed. -If the review reveals shortcomings anyone can commit and push a fix. -Commonly the person to do this is the creator of the merge/pull request. -The diff in the merge/pull requests automatically updates when new commits are pushed on the branch. +The merge request serves as a code review tool, and no separate code review tools should be needed. +If the review reveals shortcomings, anyone can commit and push a fix. +Usually, the person to do this is the creator of the merge request. +The diff in the merge request automatically updates when new commits are pushed to the branch. + +When you are ready for your feature branch to be merged, assign the merge request to the person who knows most about the codebase you are changing. +Also, mention any other people from whom you would like feedback. +After the assigned person feels comfortable with the result, they can merge the branch. +If the assigned person does not feel comfortable, they can request more changes or close the merge request without merging. + +In GitLab, it is common to protect the long-lived branches, e.g., the `master` branch, so that [most developers can't modify them](../permissions/permissions.md). +So, if you want to merge into a protected branch, assign your merge request to someone with maintainer permissions. -When you feel comfortable with it to be merged you assign it to the person that knows most about the codebase you are changing and mention any other people you would like feedback from. -There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. -If the assigned person does not feel comfortable they can close the merge request without merging. +After you merge a feature branch, you should remove it from the source control software. +In GitLab, you can do this when merging. +Removing finished branches ensures that the list of branches shows only work in progress. +It also ensures that if someone reopens the issue, they can use the same branch name without causing problems. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html). -So if you want to merge it into a protected branch you assign it to someone with maintainer authorizations. +NOTE: **Note:** +When you reopen an issue you need to create a new merge request. + +![Remove checkbox for branch in merge requests](remove_checkbox.png) ## Issue tracking with GitLab flow -![Merge request with the branch name 15-require-a-password-to-change-it and assignee field shown](merge_request.png) +![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](merge_request.png) GitLab flow is a way to make the relation between the code and the issue tracker more transparent. -Any significant change to the code should start with an issue where the goal is described. -Having a reason for every code change is important to inform everyone on the team and to help people keep the scope of a feature branch small. -In GitLab each change to the codebase starts with an issue in the issue tracking system. -If there is no issue yet it should be created first provided there is significant work involved (more than 1 hour). -For many organizations this will be natural since the issue will have to be estimated for the sprint. -Issue titles should describe the desired state of the system, e.g. "As an administrator I want to remove users without receiving an error" instead of "Admin can't remove users.". - -When you are ready to code you start a branch for the issue from the master branch. -The name of this branch should start with the issue number, for example '15-require-a-password-to-change-it'. - -When you are done or want to discuss the code you open a merge request. -This is an online place to discuss the change and review the code. -Opening a merge request is a manual action since you do not always want to merge a new branch you push, it could be a long-running environment or release branch. -If you open the merge request but do not assign it to anyone it is a 'Work In Progress' merge request. -These are used to discuss the proposed implementation but are not ready for inclusion in the master branch yet. -_Pro tip:_ Start the title of the merge request with `[WIP]` or `WIP:` to prevent it from being merged before it's ready. - -When the author thinks the code is ready the merge request is assigned to reviewer. -The reviewer presses the merge button when they think the code is ready for inclusion in the master branch. -In this case the code is merged and a merge commit is generated that makes this event easily visible later on. -Merge requests always create a merge commit even when the commit could be added without one. -This merge strategy is called 'no fast-forward' in git. -After the merge the feature branch is deleted since it is no longer needed, in GitLab this deletion is an option when merging. +Any significant change to the code should start with an issue that describes the goal. +Having a reason for every code change helps to inform the rest of the team and to keep the scope of a feature branch small. +In GitLab, each change to the codebase starts with an issue in the issue tracking system. +If there is no issue yet, create the issue, as long as the change will take a significant amount of work, i.e., more than 1 hour. +In many organizations, raising an issue is part of the development process because they are used in sprint planning. +The issue title should describe the desired state of the system. +For example, the issue title "As an administrator, I want to remove users without receiving an error" is better than "Admin can't remove users." + +When you are ready to code, create a branch for the issue from the `master` branch. +This branch is the place for any work related to this change. + +NOTE: **Note:** +The name of a branch might be dictated by organizational standards. +For example, in GitLab, any branches in GitLab EE that are equivalent to branches in GitLab CE [must end in `-ee`](https://docs.gitlab.com/ee/development/automatic_ce_ee_merge.html#cherry-picking-from-ce-to-ee). + +When you are done or want to discuss the code, open a merge request. +A merge request is an online place to discuss the change and review the code. + +If you open the merge request but do not assign it to anyone, it is a "Work In Progress" merge request. +These are used to discuss the proposed implementation but are not ready for inclusion in the `master` branch yet. +Start the title of the merge request with "[WIP]" or "WIP:" to prevent it from being merged before it's ready. + +When you think the code is ready, assign the merge request to a reviewer. +The reviewer can merge the changes when they think the code is ready for inclusion in the `master` branch. +When they press the merge button, GitLab merges the code and creates a merge commit that makes this event easily visible later on. +Merge requests always create a merge commit, even when the branch could be merged without one. +This merge strategy is called "no fast-forward" in Git. +After the merge, delete the feature branch since it is no longer needed. +In GitLab, this deletion is an option when merging. Suppose that a branch is merged but a problem occurs and the issue is reopened. -In this case it is no problem to reuse the same branch name since it was deleted when the branch was merged. -At any time there is at most one branch for every issue. +In this case, it is no problem to reuse the same branch name since the first branch was deleted when it was merged. +At any time, there is at most one branch for every issue. It is possible that one feature branch solves more than one issue. ## Linking and closing issues from merge requests ![Merge request showing the linked issues that will be closed](close_issue_mr.png) -Linking to issues can happen by mentioning them in commit messages (fixes #14, closes #67, etc.) or in the merge request description. -GitLab then creates links to the mentioned issues and creates comments in the corresponding issues linking back to the merge request. +Link to issues by mentioning them in commit messages or the description of a merge request, for example, "Fixes #16" or "Duck typing is preferred. See #12." +GitLab then creates links to the mentioned issues and creates comments in the issues linking back to the merge request. -These issues are closed once code is merged into the default branch. +To automatically close linked issues, mention them with the words "fixes" or "closes," for example, "fixes #14" or "closes #67." GitLab closes these issues when the code is merged into the default branch. -If you only want to make the reference without closing the issue you can also just mention it: "Duck typing is preferred. #12". - -If you have an issue that spans across multiple repositories, the best thing is to create an issue for each repository and link all issues to a parent issue. +If you have an issue that spans across multiple repositories, create an issue for each repository and link all issues to a parent issue. ## Squashing commits with rebase ![Vim screen showing the rebase view](rebase.png) -With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. -In GitLab EE and .com you can also [rebase before merge](http://docs.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. -This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. -However you should never rebase commits you have pushed to a remote server. -Somebody can have referred to the commits or cherry-picked them. -When you rebase you change the identifier (SHA-1) of the commit and this is confusing. -If you do that the same change will be known under multiple identifiers and this can cause much confusion. -If people already reviewed your code it will be hard for them to review only the improvements you made since then if you have rebased everything into one commit. -Another reasons not to rebase is that you lose authorship information, maybe someone created a merge request, another person pushed a commit on there to improve it and a third one merged it. -In this case rebasing all the commits into one prevent the other authors from being properly attributed and sharing part of the [git blame](https://git-scm.com/docs/git-blame). - -People are encouraged to commit often and to frequently push to the remote repository so other people are aware what everyone is working on. -This will lead to many commits per change which makes the history harder to understand. -But the advantages of having stable identifiers outweigh this drawback. -And to understand a change in context one can always look at the merge commit that groups all the commits together when the code is merged into the master branch. - -After you merge multiple commits from a feature branch into the master branch this is harder to undo. -If you had squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed. -Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git. -This however, requires having specific merge commits for the commits your want to revert. -If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise. - -Being able to revert a merge is a good reason always to create a merge commit when you merge manually with the `--no-ff` option. -Git management software will always create a merge commit when you accept a merge request. - -## Do not order commits with rebase +With Git, you can use an interactive rebase (`rebase -i`) to squash multiple commits into one or reorder them. +This functionality is useful if you want to replace a couple of small commits with a single commit, or if you want to make the order more logical. + +However, you should never rebase commits you have pushed to a remote server. +Rebasing creates new commits for all your changes, which can cause confusion because the same change would have multiple identifiers. +It also causes merge errors for anyone working on the same branch because their history would not match with yours. +Also, if someone has already reviewed your code, rebasing makes it hard to tell what changed since the last review. + +You should also never rebase commits authored by other people. +Not only does this rewrite history, but it also loses authorship information. +Rebasing prevents the other authors from being attributed and sharing part of the [`git blame`](https://git-scm.com/docs/git-blame). + +If a merge involves many commits, it may seem more difficult to undo. +You might think to solve this by squashing all the changes into one commit before merging, but as discussed earlier, it is a bad idea to rebase commits that you have already pushed. +Fortunately, there is an easy way to undo a merge with all its commits. +The way to do this is by reverting the merge commit. +Preserving this ability to revert a merge is a good reason to always use the "no fast-forward" (`--no-ff`) strategy when you merge manually. + +NOTE: **Note:** +If you revert a merge commit and then change your mind, revert the revert commit to redo the merge. +Git does not allow you to merge the code again otherwise. + +## Reducing merge commits in feature branches ![List of sequential merge commits](merge_commits.png) -With git you can also rebase your feature branch commits to order them after the commits on the master branch. -This prevents creating a merge commit when merging master into your feature branch and creates a nice linear history. -However, just like with squashing you should never rebase commits you have pushed to a remote server. -This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend. -When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](https://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/). -You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set. -There has to be a better way to avoid many merge commits. - -The way to prevent creating many merge commits is to not frequently merge master into the feature branch. -We'll discuss the three reasons to merge in master: leveraging code, merge conflicts, and long running branches. -If you need to leverage some code that was introduced in master after you created the feature branch you can sometimes solve this by just cherry-picking a commit. -If your feature branch has a merge conflict, creating a merge commit is a normal way of solving this. -You can prevent some merge conflicts by using [gitattributes](http://git-scm.com/docs/gitattributes) for files that can be in a random order. -For example in GitLab our changelog file is specified in .gitattributes as `CHANGELOG.md merge=union` so that there are fewer merge conflicts in it. -The last reason for creating merge commits is having long lived branches that you want to keep up to date with the latest state of the project. -Martin Fowler, in [his article about feature branches](http://martinfowler.com/bliki/FeatureBranch.html) talks about this Continuous Integration (CI). -At GitLab we are guilty of confusing CI with branch testing. Quoting Martin Fowler: "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit. -That's continuous building, and a Good Thing, but there's no integration, so it's not CI.". -The solution to prevent many merge commits is to keep your feature branches short-lived, the vast majority should take less than one day of work. -If your feature branches commonly take more than a day of work, look into ways to create smaller units of work and/or use [feature toggles](http://martinfowler.com/bliki/FeatureToggle.html). -As for the long running branches that take more than one day there are two strategies. -In a CI strategy you can merge in master at the start of the day to prevent painful merges at a later time. -In a synchronization point strategy you only merge in from well defined points in time, for example a tagged release. -This strategy is [advocated by Linus Torvalds](https://www.mail-archive.com/dri-devel@lists.sourceforge.net/msg39091.html) because the state of the code at these points is better known. - -In conclusion, we can say that you should try to prevent merge commits, but not eliminate them. -Your codebase should be clean but your history should represent what actually happened. -Developing software happen in small messy steps and it is OK to have your history reflect this. -You can use tools to view the network graphs of commits and understand the messy history that created your code. -If you rebase code the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers. +Having lots of merge commits can make your repository history messy. +Therefore, you should try to avoid merge commits in feature branches. +Often, people avoid merge commits by just using rebase to reorder their commits after the commits on the `master` branch. +Using rebase prevents a merge commit when merging `master` into your feature branch, and it creates a neat linear history. +However, as discussed in [the section about rebasing](#squashing-commits-with-rebase), you should never rebase commits you have pushed to a remote server. +This restriction makes it impossible to rebase work in progress that you already shared with your team, which is something we recommend. -## Award emojis on issues and merge requests +Rebasing also creates more work, since every time you rebase, you have to resolve similar conflicts. +Sometimes you can reuse recorded resolutions (`rerere`), but merging is better since you only have to resolve conflicts once. +Atlassian has a more thorough explanation of the tradeoffs between merging and rebasing [on their blog](https://www.atlassian.com/blog/git/git-team-workflows-merge-or-rebase). -![Emoji bar in GitLab](award_emoji.png) +A good way to prevent creating many merge commits is to not frequently merge `master` into the feature branch. +There are three reasons to merge in `master`: utilizing new code, resolving merge conflicts, and updating long-running branches. -It is common to voice approval or disapproval by using +1 or -1. In GitLab you -can use emojis to give a virtual high five on issues and merge requests. +If you need to utilize some code that was introduced in `master` after you created the feature branch, you can often solve this by just cherry-picking a commit. -## Pushing and removing branches +If your feature branch has a merge conflict, creating a merge commit is a standard way of solving this. -![Remove checkbox for branch in merge requests](remove_checkbox.png) +NOTE: **Note:** +Sometimes you can use .gitattributes to reduce merge conflicts. +For example, you can set your changelog file to use the [union merge driver](https://git-scm.com/docs/gitattributes#gitattributes-union) so that multiple new entries don't conflict with each other. -We recommend that people push their feature branches frequently, even when they are not ready for review yet. -By doing this you prevent team members from accidentally starting to work on the same issue. -Of course this situation should already be prevented by assigning someone to the issue in the issue tracking software. -However sometimes one of the two parties forgets to assign someone in the issue tracking software. -After a branch is merged it should be removed from the source control software. -In GitLab and similar systems this is an option when merging. -This ensures that the branch overview in the repository management software shows only work in progress. -This also ensures that when someone reopens the issue a new branch with the same name can be used without problem. -When you reopen an issue you need to create a new merge request. +The last reason for creating merge commits is to keep long-running feature branches up-to-date with the latest state of the project. +The solution here is to keep your feature branches short-lived. +Most feature branches should take less than one day of work. +If your feature branches often take more than a day of work, try to split your features into smaller units of work. + +If you need to keep a feature branch open for more than a day, there are a few strategies to keep it up-to-date. +One option is to use continuous integration (CI) to merge in `master` at the start of the day. +Another option is to only merge in from well-defined points in time, for example, a tagged release. +You could also use [feature toggles](https://martinfowler.com/bliki/FeatureToggle.html) to hide incomplete features so you can still merge back into `master` every day. + +> **Note:** Don't confuse automatic branch testing with continuous integration. +> Martin Fowler makes this distinction in [his article about feature branches](https://martinfowler.com/bliki/FeatureBranch.html): +> +> "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit. +> That's continuous building, and a Good Thing, but there's no *integration*, so it's not CI." + +In conclusion, you should try to prevent merge commits, but not eliminate them. +Your codebase should be clean, but your history should represent what actually happened. +Developing software happens in small, messy steps, and it is OK to have your history reflect this. +You can use tools to view the network graphs of commits and understand the messy history that created your code. +If you rebase code, the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers. + +## Commit often and push frequently + +Another way to make your development work easier is to commit often. +Every time you have a working set of tests and code, you should make a commit. +Splitting up work into individual commits provides context for developers looking at your code later. +Smaller commits make it clear how a feature was developed, and they make it easy to roll back to a specific good point in time or to revert one code change without reverting several unrelated changes. + +Committing often also makes it easy to share your work, which is important so that everyone is aware of what you are working on. +You should push your feature branch frequently, even when it is not yet ready for review. +By sharing your work in a feature branch or [a merge request](#mergepull-requests-with-gitlab-flow), you prevent your team members from duplicating work. +Sharing your work before it's complete also allows for discussion and feedback about the changes, which can help improve the code before it gets to review. -## Committing often and with the right message +## How to write a good commit message ![Good and bad commit message](good_commit.png) -We recommend to commit early and often. -Each time you have a functioning set of tests and code a commit can be made. -The advantage is that when an extension or refactor goes wrong it is easy to revert to a working version. -This is quite a change for programmers that used SVN before, they used to commit when their work was ready to share. -The trick is to use the merge/pull request with multiple commits when your work is ready to share. -The commit message should reflect your intention, not the contents of the commit. -The contents of the commit can be easily seen anyway, the question is why you did it. -An example of a good commit message is: "Combine templates to dry up the user views.". -Some words that are bad commit messages because they don't contain much information are: change, improve and refactor. -The word fix or fixes is also a red flag, unless it comes after the commit sentence and references an issue number. -To see more information about the formatting of commit messages please see this great [blog post by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). +A commit message should reflect your intention, not just the contents of the commit. +It is easy to see the changes in a commit, so the commit message should explain why you made those changes. +An example of a good commit message is: "Combine templates to reduce duplicate code in the user views." +The words "change," "improve," "fix," and "refactor" don't add much information to a commit message. +For example, "Improve XML generation" could be better written as "Properly escape special characters in XML generation." +For more information about formatting commit messages, please see this excellent [blog post by Tim Pope](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). ## Testing before merging -![Merge requests showing the test states, red, yellow and green](ci_mr.png) - -In old workflows the Continuous Integration (CI) server commonly ran tests on the master branch only. -Developers had to ensure their code did not break the master branch. -When using GitLab flow developers create their branches from this master branch so it is essential it is green. -Therefore each merge request must be tested before it is accepted. -CI software like Travis and GitLab CI show the build results right in the merge request itself to make this easy. -One drawback is that they are testing the feature branch itself and not the merged result. -What one can do to improve this is to test the merged result itself. -The problem is that the merge result changes every time something is merged into master. -Retesting on every commit to master is computationally expensive and means you are more frequently waiting for test results. -If there are no merge conflicts and the feature branches are short lived the risk is acceptable. -If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests. -If you have long lived feature branches that last for more than a few days you should make your issues smaller. +![Merge requests showing the test states: red, yellow, and green](ci_mr.png) -## Working with feature branches +In old workflows, the continuous integration (CI) server commonly ran tests on the `master` branch only. +Developers had to ensure their code did not break the `master` branch. +When using GitLab flow, developers create their branches from this `master` branch, so it is essential that it never breaks. +Therefore, each merge request must be tested before it is accepted. +CI software like Travis CI and GitLab CI show the build results right in the merge request itself to make this easy. -![Shell output showing git pull output](git_pull.png) +There is one drawback to testing merge requests: the CI server only tests the feature branch itself, not the merged result. +Ideally, the server could also test the `master` branch after each change. +However, retesting on every commit to `master` is computationally expensive and means you are more frequently waiting for test results. +Since feature branches should be short-lived, testing just the branch is an acceptable risk. +If new commits in `master` cause merge conflicts with the feature branch, merge `master` back into the branch to make the CI server re-run the tests. +As said before, if you often have feature branches that last for more than a few days, you should make your issues smaller. -When initiating a feature branch, always start with an up to date master to branch off from. -If you know beforehand that your work absolutely depends on another branch you can also branch from there. -If you need to merge in another branch after starting explain the reason in the merge commit. -If you have not pushed your commits to a shared location yet you can also rebase on master or another feature branch. -Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](https://lwn.net/Articles/328438/). -Merging only when needed prevents creating merge commits in your feature branch that later end up littering the master history. +## Working with feature branches -### References +![Shell output showing git pull output](git_pull.png) -- [Git Flow by Vincent Driessen](http://nvie.com/posts/a-successful-git-branching-model/) +When creating a feature branch, always branch from an up-to-date `master`. +If you know before you start that your work depends on another branch, you can also branch from there. +If you need to merge in another branch after starting, explain the reason in the merge commit. +If you have not pushed your commits to a shared location yet, you can also incorporate changes by rebasing on `master` or another feature branch. +Do not merge from upstream again if your code can work and merge cleanly without doing so. +Merging only when needed prevents creating merge commits in your feature branch that later end up littering the `master` history. diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md index 2bb140b99d0..ad43189148c 100644 --- a/doc/workflow/time_tracking.md +++ b/doc/workflow/time_tracking.md @@ -38,13 +38,14 @@ Adding time entries (time spent or estimates) is limited to project members. To enter an estimate, write `/estimate`, followed by the time. For example, if you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write -`/estimate 3d 5h 10m`. +`/estimate 3d 5h 10m`. Time units that we support are listed at the bottom of +this help page. Every time you enter a new time estimate, any previous time estimates will be overridden by this new value. There should only be one valid estimate in an issue or a merge request. -To remove an estimation entirely, use `/remove_estimation`. +To remove an estimation entirely, use `/remove_estimate`. ### Time spent @@ -72,5 +73,8 @@ The following time units are available: Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h. -[landing]: https://about.gitlab.com/features/time-tracking +Other interesting links: + +- [Time Tracking landing page on about.gitlab.com](https://about.gitlab.com/features/time-tracking) + [quick actions]: ../user/project/quick_actions.md diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 9d23daafe95..8defc59224d 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -323,6 +323,22 @@ module API present paginate(commit.merge_requests), with: Entities::MergeRequestBasic end + + desc "Get a commit's GPG signature" do + success Entities::CommitSignature + end + params do + requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' + end + get ':id/repository/commits/:sha/signature', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do + commit = user_project.commit(params[:sha]) + not_found! 'Commit' unless commit + + signature = commit.signature + not_found! 'GPG Signature' unless signature + + present signature, with: Entities::CommitSignature + end end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index beb8ce349b4..27da2c2e5ed 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -369,8 +369,9 @@ module API end class Commit < Grape::Entity - expose :id, :short_id, :title, :created_at + expose :id, :short_id, :created_at expose :parent_ids + expose :full_title, as: :title expose :safe_message, as: :message expose :author_name, :author_email, :authored_date expose :committer_name, :committer_email, :committed_date @@ -391,6 +392,13 @@ module API expose :project_id end + class CommitSignature < Grape::Entity + expose :gpg_key_id + expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email + expose :verification_status + expose :gpg_key_subkey_id + end + class BasicRef < Grape::Entity expose :type, :name end @@ -731,6 +739,12 @@ module API def build_available?(options) options[:project]&.feature_available?(:builds, options[:current_user]) end + + expose :user do + expose :can_merge do |merge_request, options| + merge_request.can_be_merged_by?(options[:current_user]) + end + end end class MergeRequestChanges < MergeRequest @@ -1003,7 +1017,7 @@ module API end class LabelBasic < Grape::Entity - expose :id, :name, :color, :description + expose :id, :name, :color, :description, :text_color end class Label < LabelBasic @@ -1031,6 +1045,9 @@ module API expose :priority do |label, options| label.priority(options[:parent]) end + expose :is_project_label do |label, options| + label.is_a?(::ProjectLabel) + end end class List < Grape::Entity diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index 4eaaca96b49..fe78049af87 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -81,6 +81,14 @@ module API Gitlab::GlRepository.gl_repository(project, wiki?) end + def gl_project_path + if wiki? + project.wiki.full_path + else + project.full_path + end + end + # Return the repository depending on whether we want the wiki or the # regular repository def repository diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 9488b3469d9..70b32f7d758 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -77,6 +77,7 @@ module API when ::Gitlab::GitAccessResult::Success payload = { gl_repository: gl_repository, + gl_project_path: gl_project_path, gl_id: Gitlab::GlId.gl_id(user), gl_username: user&.username, git_config_options: [], @@ -117,13 +118,7 @@ module API raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!") end - token_handler = Gitlab::LfsToken.new(actor) - - { - username: token_handler.actor_name, - lfs_token: token_handler.token, - repository_http_path: project.http_url_to_repo - } + Gitlab::LfsToken.new(actor).authentication_payload(project.http_url_to_repo) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/services.rb b/lib/api/services.rb index 36bdba2d765..163c7505a65 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -368,8 +368,9 @@ module API name: :webhook, type: String, desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…' - } - ], + }, + CHAT_NOTIFICATION_EVENTS + ].flatten, 'irker' => [ { required: true, diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index ef0e3decc2c..994074ddc67 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -11,9 +11,7 @@ module API } end - params :wiki_page_params do - requires :content, type: String, desc: 'Content of a wiki page' - requires :title, type: String, desc: 'Title of a wiki page' + params :common_wiki_page_params do optional :format, type: String, values: ProjectWiki::MARKUPS.values.map(&:to_s), @@ -54,7 +52,9 @@ module API success Entities::WikiPage end params do - use :wiki_page_params + requires :title, type: String, desc: 'Title of a wiki page' + requires :content, type: String, desc: 'Content of a wiki page' + use :common_wiki_page_params end post ':id/wikis' do authorize! :create_wiki, user_project @@ -72,7 +72,10 @@ module API success Entities::WikiPage end params do - use :wiki_page_params + optional :title, type: String, desc: 'Title of a wiki page' + optional :content, type: String, desc: 'Content of a wiki page' + use :common_wiki_page_params + at_least_one_of :content, :title, :format end put ':id/wikis/:slug' do authorize! :create_wiki, user_project diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb index 97527976437..de133774dfa 100644 --- a/lib/banzai/filter/footnote_filter.rb +++ b/lib/banzai/filter/footnote_filter.rb @@ -29,21 +29,30 @@ module Banzai # Sanitization stripped off the section wrapper - add it back in first_footnote.parent.wrap('<section class="footnotes">') rand_suffix = "-#{random_number}" + modified_footnotes = {} doc.css('sup > a[id]').each do |link_node| ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX) footnote_node = doc.at_css("li[id=#{fn_id(ref_num)}]") - backref_node = footnote_node.at_css("a[href=\"##{fnref_id(ref_num)}\"]") - if ref_num =~ INTEGER_PATTERN && footnote_node && backref_node - link_node[:href] += rand_suffix - link_node[:id] += rand_suffix - footnote_node[:id] += rand_suffix - backref_node[:href] += rand_suffix + if INTEGER_PATTERN.match?(ref_num) && (footnote_node || modified_footnotes[ref_num]) + link_node[:href] += rand_suffix + link_node[:id] += rand_suffix # Sanitization stripped off class - add it back in link_node.parent.append_class('footnote-ref') - backref_node.append_class('footnote-backref') + + unless modified_footnotes[ref_num] + footnote_node[:id] += rand_suffix + backref_node = footnote_node.at_css("a[href=\"##{fnref_id(ref_num)}\"]") + + if backref_node + backref_node[:href] += rand_suffix + backref_node.append_class('footnote-backref') + end + + modified_footnotes[ref_num] = true + end end end diff --git a/lib/gitlab/ci/build/policy/changes.rb b/lib/gitlab/ci/build/policy/changes.rb index 1663c875426..9c705a1cd3e 100644 --- a/lib/gitlab/ci/build/policy/changes.rb +++ b/lib/gitlab/ci/build/policy/changes.rb @@ -10,7 +10,7 @@ module Gitlab end def satisfied_by?(pipeline, seed) - return true unless pipeline.branch_updated? + return true if pipeline.modified_paths.nil? pipeline.modified_paths.any? do |path| @globs.any? do |glob| diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb new file mode 100644 index 00000000000..0f3f5cb3c08 --- /dev/null +++ b/lib/gitlab/danger/helper.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true +require 'net/http' +require 'json' + +require_relative 'teammate' + +module Gitlab + module Danger + module Helper + ROULETTE_DATA_URL = URI.parse('https://about.gitlab.com/roulette.json').freeze + + # Returns a list of all files that have been added, modified or renamed. + # `git.modified_files` might contain paths that already have been renamed, + # so we need to remove them from the list. + # + # Considering these changes: + # + # - A new_file.rb + # - D deleted_file.rb + # - M modified_file.rb + # - R renamed_file_before.rb -> renamed_file_after.rb + # + # it will return + # ``` + # [ 'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb' ] + # ``` + # + # @return [Array<String>] + def all_changed_files + Set.new + .merge(git.added_files.to_a) + .merge(git.modified_files.to_a) + .merge(git.renamed_files.map { |x| x[:after] }) + .subtract(git.renamed_files.map { |x| x[:before] }) + .to_a + .sort + end + + def ee? + ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md') + end + + def project_name + ee? ? 'gitlab-ee' : 'gitlab-ce' + end + + # Looks up the current list of GitLab team members and parses it into a + # useful form + # + # @return [Array<Teammate>] + def team + @team ||= + begin + rsp = Net::HTTP.get_response(ROULETTE_DATA_URL) + raise "Failed to read #{ROULETTE_DATA_URL}: #{rsp.code} #{rsp.message}" unless + rsp.is_a?(Net::HTTPSuccess) + + data = JSON.parse(rsp.body) + data.map { |hash| ::Gitlab::Danger::Teammate.new(hash) } + rescue JSON::ParserError + raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}" + end + end + + # Like +team+, but only returns teammates in the current project, based on + # project_name. + # + # @return [Array<Teammate>] + def project_team + team.select { |member| member.in_project?(project_name) } + end + + # @return [Hash<String,Array<String>>] + def changes_by_category + all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash| + hash[category_for_file(file)] << file + end + end + + # Determines the category a file is in, e.g., `:frontend` or `:backend` + # @return[Symbol] + def category_for_file(file) + _, category = CATEGORIES.find { |regexp, _| regexp.match?(file) } + + category || :unknown + end + + # Returns the GFM for a category label, making its best guess if it's not + # a category we know about. + # + # @return[String] + def label_for_category(category) + CATEGORY_LABELS.fetch(category, "~#{category}") + end + + CATEGORY_LABELS = { + docs: "~Documentation", + none: "", + qa: "~QA" + }.freeze + + # rubocop:disable Style/RegexpLiteral + CATEGORIES = { + %r{\Adoc/} => :docs, + %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs, + + %r{\A(ee/)?app/(assets|views)/} => :frontend, + %r{\A(ee/)?public/} => :frontend, + %r{\A(ee/)?spec/javascripts/} => :frontend, + %r{\A(ee/)?vendor/assets/} => :frontend, + %r{\A(jest\.config\.js|package\.json|yarn\.lock)\z} => :frontend, + + %r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend, + %r{\A(ee/)?(bin|config|danger|generator_templates|lib|rubocop|scripts)/} => :backend, + %r{\A(ee/)?spec/(?!javascripts)[^/]+} => :backend, + %r{\A(ee/)?vendor/(?!assets)[^/]+} => :backend, + %r{\A(ee/)?vendor/(languages\.yml|licenses\.csv)\z} => :backend, + %r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile|\.gitlab-ci\.yml)\z} => :backend, + %r{\A[A-Z_]+_VERSION\z} => :backend, + + %r{\A(ee/)?db/} => :database, + %r{\A(ee/)?qa/} => :qa, + + # Files that don't fit into any category are marked with :none + %r{\A(ee/)?changelogs/} => :none, + + # Fallbacks in case the above patterns miss anything + %r{\.rb\z} => :backend, + %r{\.(md|txt)\z} => :docs, + %r{\.js\z} => :frontend + }.freeze + # rubocop:enable Style/RegexpLiteral + end + end +end diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb new file mode 100644 index 00000000000..4b822aa86c5 --- /dev/null +++ b/lib/gitlab/danger/teammate.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Danger + class Teammate + attr_reader :name, :username, :projects + + def initialize(options = {}) + @name = options['name'] + @username = options['username'] + @projects = options['projects'] + end + + def markdown_name + "[#{name}](https://gitlab.com/#{username}) (`@#{username}`)" + end + + def in_project?(name) + projects&.has_key?(name) + end + + # Traintainers also count as reviewers + def reviewer?(project, category) + capabilities(project) == "reviewer #{category}" || traintainer?(project, category) + end + + def traintainer?(project, category) + capabilities(project) == "trainee_maintainer #{category}" + end + + def maintainer?(project, category) + capabilities(project) == "maintainer #{category}" + end + + private + + def capabilities(project) + projects.fetch(project, '') + end + end + end +end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index e410d5a8333..c9d89d56884 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -293,6 +293,10 @@ module Gitlab end end + def viewer + rich_viewer || simple_viewer + end + def simple_viewer @simple_viewer ||= simple_viewer_class.new(self) end diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index 08e30214b46..0891f79198d 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -52,6 +52,14 @@ module Gitlab Gitlab::EtagCaching::Router::Route.new( %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z), 'environments' + ), + Gitlab::EtagCaching::Router::Route.new( + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/github/realtime_changes\.json\z), + 'realtime_changes_import_github' + ), + Gitlab::EtagCaching::Router::Route.new( + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/import/gitea/realtime_changes\.json\z), + 'realtime_changes_import_gitea' ) ].freeze diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 54bbd531398..593a3676519 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -491,6 +491,13 @@ module Gitlab end end + # Return total diverging commits count + def diverging_commit_count(from, to, max_count:) + wrapped_gitaly_errors do + gitaly_commit_client.diverging_commit_count(from, to, max_count: max_count) + end + end + # Mimic the `git clean` command and recursively delete untracked files. # Valid keys that can be passed in the +options+ hash are: # diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 4e46cb9f05c..ea12424eb4a 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -150,6 +150,17 @@ module Gitlab GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count end + def diverging_commit_count(from, to, max_count:) + request = Gitaly::CountDivergingCommitsRequest.new( + repository: @gitaly_repo, + from: encode_binary(from), + to: encode_binary(to), + max_count: max_count + ) + response = GitalyClient.call(@repository.storage, :commit_service, :count_diverging_commits, request, timeout: GitalyClient.medium_timeout) + [response.left_count, response.right_count] + end + def list_last_commits_for_tree(revision, path, offset: 0, limit: 25) request = Gitaly::ListLastCommitsForTreeRequest.new( repository: @gitaly_repo, diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb index ae7c4cf1b38..e294173f992 100644 --- a/lib/gitlab/github_import/importer/pull_request_importer.rb +++ b/lib/gitlab/github_import/importer/pull_request_importer.rb @@ -67,6 +67,36 @@ module Gitlab def insert_git_data(merge_request, already_exists) insert_or_replace_git_data(merge_request, pull_request.source_branch_sha, pull_request.target_branch_sha, already_exists) + # We need to create the branch after the merge request is + # populated to ensure the merge request is in the right state + # when the branch is created. + create_source_branch_if_not_exists(merge_request) + end + + # An imported merge request will not be mergeable unless the + # source branch exists. For pull requests from forks, the source + # branch will be in the form of + # "github/fork/{project-name}/{source_branch}". This branch will never + # exist, so we create it here. + # + # Note that we only create the branch if the merge request is still open. + # For projects that have many pull requests, we assume that if it's closed + # the branch has already been deleted. + def create_source_branch_if_not_exists(merge_request) + return unless merge_request.open? + + source_branch = pull_request.formatted_source_branch + + return if project.repository.branch_exists?(source_branch) + + project.repository.add_branch(merge_request.author, source_branch, pull_request.source_branch_sha) + rescue Gitlab::Git::CommandError => e + Gitlab::Sentry.track_acceptable_exception(e, + extra: { + source_branch: source_branch, + project_id: merge_request.project.id, + merge_request_id: merge_request.id + }) end end end diff --git a/lib/gitlab/github_import/representation/pull_request.rb b/lib/gitlab/github_import/representation/pull_request.rb index 593b491a837..0ccc4bfaed3 100644 --- a/lib/gitlab/github_import/representation/pull_request.rb +++ b/lib/gitlab/github_import/representation/pull_request.rb @@ -76,10 +76,10 @@ module Gitlab # Returns a formatted source branch. # # For cross-project pull requests the branch name will be in the format - # `owner-name:branch-name`. + # `github/fork/owner-name/branch-name`. def formatted_source_branch if cross_project? && source_repository_owner - "#{source_repository_owner}:#{source_branch}" + "github/fork/#{source_repository_owner}/#{source_branch}" elsif source_branch == target_branch # Sometimes the source and target branch are the same, but GitLab # doesn't support this. This can happen when both the user and diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb index d638d2b43ee..2a3d790d67b 100644 --- a/lib/gitlab/graphql/authorize/instrumentation.rb +++ b/lib/gitlab/graphql/authorize/instrumentation.rb @@ -35,10 +35,22 @@ module Gitlab private def build_checker(current_user, abilities) - proc do |obj| + lambda do |value| # Load the elements if they weren't loaded by BatchLoader yet - obj = obj.sync if obj.respond_to?(:sync) - obj if abilities.all? { |ability| Ability.allowed?(current_user, ability, obj) } + value = value.sync if value.respond_to?(:sync) + + check = lambda do |object| + abilities.all? do |ability| + Ability.allowed?(current_user, ability, object) + end + end + + case value + when Array + value.select(&check) + else + value if check.call(value) + end end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 099677a791c..7f8c6d56627 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -133,7 +133,6 @@ excluded_attributes: - :external_diff - :stored_externally - :external_diff_store - - :st_diffs merge_request_diff_files: - :diff - :external_diff_offset diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index 947caaaefee..725c1101d70 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -61,7 +61,7 @@ module Gitlab def log_base_data { importer: 'Import/Export', - import_jid: @project&.import_state&.import_jid, + import_jid: @project&.import_state&.jid, project_id: @project&.id, project_path: @project&.full_path } diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb index 26b81847d37..31e6fc9d8c7 100644 --- a/lib/gitlab/lfs_token.rb +++ b/lib/gitlab/lfs_token.rb @@ -30,8 +30,8 @@ module Gitlab end end - def token(expire_time: DEFAULT_EXPIRE_TIME) - HMACToken.new(actor).token(expire_time) + def token + HMACToken.new(actor).token(DEFAULT_EXPIRE_TIME) end def token_valid?(token_to_check) @@ -47,6 +47,15 @@ module Gitlab user? ? :lfs_token : :lfs_deploy_token end + def authentication_payload(repository_http_path) + { + username: actor_name, + lfs_token: token, + repository_http_path: repository_http_path, + expires_in: DEFAULT_EXPIRE_TIME + } + end + private # rubocop:disable Lint/UselessAccessModifier class HMACToken diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index 651e241362c..ff3fffe7b95 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -19,7 +19,7 @@ module Gitlab # Returns the name of the series to use for storing method calls. def self.series - @series ||= "#{Metrics.series_prefix}method_calls" + @series ||= "#{::Gitlab::Metrics.series_prefix}method_calls" end # Instruments a class method. @@ -118,7 +118,7 @@ module Gitlab # mod - The module containing the method. # name - The name of the method to instrument. def self.instrument(type, mod, name) - return unless Metrics.enabled? + return unless ::Gitlab::Metrics.enabled? name = name.to_sym target = type == :instance ? mod : mod.singleton_class diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb index 85438011cb9..d0c63a862c2 100644 --- a/lib/gitlab/metrics/method_call.rb +++ b/lib/gitlab/metrics/method_call.rb @@ -65,7 +65,7 @@ module Gitlab # Returns true if the total runtime of this method exceeds the method call # threshold. def above_threshold? - real_time.in_milliseconds >= Metrics.method_call_threshold + real_time.in_milliseconds >= ::Gitlab::Metrics.method_call_threshold end end end diff --git a/lib/gitlab/metrics/methods.rb b/lib/gitlab/metrics/methods.rb index 447d03bfca4..cee601ff14c 100644 --- a/lib/gitlab/metrics/methods.rb +++ b/lib/gitlab/metrics/methods.rb @@ -58,11 +58,11 @@ module Gitlab def build_metric!(type, name, options) case type when :gauge - Gitlab::Metrics.gauge(name, options.docstring, options.base_labels, options.multiprocess_mode) + ::Gitlab::Metrics.gauge(name, options.docstring, options.base_labels, options.multiprocess_mode) when :counter - Gitlab::Metrics.counter(name, options.docstring, options.base_labels) + ::Gitlab::Metrics.counter(name, options.docstring, options.base_labels) when :histogram - Gitlab::Metrics.histogram(name, options.docstring, options.base_labels, options.buckets) + ::Gitlab::Metrics.histogram(name, options.docstring, options.base_labels, options.buckets) when :summary raise NotImplementedError, "summary metrics are not currently supported" else diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb index 74c956ab5af..26aa0910047 100644 --- a/lib/gitlab/metrics/requests_rack_middleware.rb +++ b/lib/gitlab/metrics/requests_rack_middleware.rb @@ -8,15 +8,15 @@ module Gitlab end def self.http_request_total - @http_request_total ||= Gitlab::Metrics.counter(:http_requests_total, 'Request count') + @http_request_total ||= ::Gitlab::Metrics.counter(:http_requests_total, 'Request count') end def self.rack_uncaught_errors_count - @rack_uncaught_errors_count ||= Gitlab::Metrics.counter(:rack_uncaught_errors_total, 'Request handling uncaught errors count') + @rack_uncaught_errors_count ||= ::Gitlab::Metrics.counter(:rack_uncaught_errors_total, 'Request handling uncaught errors count') end def self.http_request_duration_seconds - @http_request_duration_seconds ||= Gitlab::Metrics.histogram(:http_request_duration_seconds, 'Request handling execution time', + @http_request_duration_seconds ||= ::Gitlab::Metrics.histogram(:http_request_duration_seconds, 'Request handling execution time', {}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 2.5, 5, 10, 25]) end diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb index c4c38b23a55..5138b37f83e 100644 --- a/lib/gitlab/metrics/samplers/influx_sampler.rb +++ b/lib/gitlab/metrics/samplers/influx_sampler.rb @@ -10,7 +10,7 @@ module Gitlab # statistics, etc. class InfluxSampler < BaseSampler # interval - The sampling interval in seconds. - def initialize(interval = Metrics.settings[:sample_interval]) + def initialize(interval = ::Gitlab::Metrics.settings[:sample_interval]) super(interval) @last_step = nil @@ -32,7 +32,7 @@ module Gitlab end def flush - Metrics.submit_metrics(@metrics.map(&:to_hash)) + ::Gitlab::Metrics.submit_metrics(@metrics.map(&:to_hash)) end def sample_memory_usage diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb index 232a58a7d69..18a69321905 100644 --- a/lib/gitlab/metrics/samplers/ruby_sampler.rb +++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb @@ -24,14 +24,14 @@ module Gitlab def init_metrics metrics = {} - metrics[:sampler_duration] = Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels) - metrics[:total_time] = Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels) + metrics[:sampler_duration] = ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels) + metrics[:total_time] = ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels) GC.stat.keys.each do |key| - metrics[key] = Metrics.gauge(with_prefix(:gc_stat, key), to_doc_string(key), labels, :livesum) + metrics[key] = ::Gitlab::Metrics.gauge(with_prefix(:gc_stat, key), to_doc_string(key), labels, :livesum) end - metrics[:memory_usage] = Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum) - metrics[:file_descriptors] = Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum) + metrics[:memory_usage] = ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum) + metrics[:file_descriptors] = ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum) metrics end diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb index 4c5b849cc51..bec64e864b3 100644 --- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb +++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb @@ -9,11 +9,11 @@ module Gitlab end def unicorn_active_connections - @unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max) + @unicorn_active_connections ||= ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max) end def unicorn_queued_connections - @unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max) + @unicorn_queued_connections ||= ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max) end def enabled? diff --git a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb index 56e106b9612..71a5406815f 100644 --- a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb +++ b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb @@ -9,7 +9,7 @@ module Gitlab LOG_FILENAME = File.join(Rails.root, 'log', 'sidekiq_exporter.log') def enabled? - Gitlab::Metrics.metrics_folder_present? && settings.enabled + ::Gitlab::Metrics.metrics_folder_present? && settings.enabled end def settings diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb index f633e1a9d7c..01db507761b 100644 --- a/lib/gitlab/metrics/subscribers/rails_cache.rb +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -64,7 +64,7 @@ module Gitlab end def metric_cache_operation_duration_seconds - @metric_cache_operation_duration_seconds ||= Gitlab::Metrics.histogram( + @metric_cache_operation_duration_seconds ||= ::Gitlab::Metrics.histogram( :gitlab_cache_operation_duration_seconds, 'Cache access time', Transaction::BASE_LABELS.merge({ action: nil }), @@ -73,7 +73,7 @@ module Gitlab end def metric_cache_misses_total - @metric_cache_misses_total ||= Gitlab::Metrics.counter( + @metric_cache_misses_total ||= ::Gitlab::Metrics.counter( :gitlab_cache_misses_total, 'Cache read miss', Transaction::BASE_LABELS diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 468d7cb56fc..e91803ecd62 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -64,7 +64,7 @@ module Gitlab end def add_metric(series, values, tags = {}) - @metrics << Metric.new("#{Metrics.series_prefix}#{series}", values, tags) + @metrics << Metric.new("#{::Gitlab::Metrics.series_prefix}#{series}", values, tags) end # Tracks a business level event @@ -127,7 +127,7 @@ module Gitlab hash end - Metrics.submit_metrics(submit_hashes) + ::Gitlab::Metrics.submit_metrics(submit_hashes) end def labels diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb index 96c6a0a7d28..a147e165262 100644 --- a/lib/gitlab/middleware/rails_queue_duration.rb +++ b/lib/gitlab/middleware/rails_queue_duration.rb @@ -7,6 +7,8 @@ module Gitlab module Middleware class RailsQueueDuration + GITLAB_RAILS_QUEUE_DURATION_KEY = 'GITLAB_RAILS_QUEUE_DURATION' + def initialize(app) @app = app end @@ -19,6 +21,7 @@ module Gitlab duration = Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000 trans.set(:rails_queue_duration, duration) metric_rails_queue_duration_seconds.observe(trans.labels, duration / 1_000) + env[GITLAB_RAILS_QUEUE_DURATION_KEY] = duration.round(2) end @app.call(env) diff --git a/lib/gitlab/sql/recursive_cte.rb b/lib/gitlab/sql/recursive_cte.rb index ec1f00a3a91..e45ac5d4765 100644 --- a/lib/gitlab/sql/recursive_cte.rb +++ b/lib/gitlab/sql/recursive_cte.rb @@ -48,7 +48,7 @@ module Gitlab # # alias_table - The Arel table to use as the alias. def alias_to(alias_table) - Arel::Nodes::As.new(table, alias_table) + Arel::Nodes::As.new(table, Arel::Table.new(alias_table.name.tr('.', '_'))) end # Applies the CTE to the given relation, returning a new one that will diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 6bfcf83f388..0101ccc046a 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -64,12 +64,12 @@ module Gitlab group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type), clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled), clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled), - clusters_applications_helm: count(::Clusters::Applications::Helm.installed), - clusters_applications_ingress: count(::Clusters::Applications::Ingress.installed), - clusters_applications_cert_managers: count(::Clusters::Applications::CertManager.installed), - clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.installed), - clusters_applications_runner: count(::Clusters::Applications::Runner.installed), - clusters_applications_knative: count(::Clusters::Applications::Knative.installed), + clusters_applications_helm: count(::Clusters::Applications::Helm.available), + clusters_applications_ingress: count(::Clusters::Applications::Ingress.available), + clusters_applications_cert_managers: count(::Clusters::Applications::CertManager.available), + clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.available), + clusters_applications_runner: count(::Clusters::Applications::Runner.available), + clusters_applications_knative: count(::Clusters::Applications::Knative.available), in_review_folder: count(::Environment.in_review_folder), groups: count(Group), issues: count(Issue), @@ -90,8 +90,14 @@ module Gitlab todos: count(Todo), uploads: count(Upload), web_hooks: count(WebHook) - }.merge(services_usage).merge(approximate_counts) - } + } + .merge(services_usage) + .merge(approximate_counts) + }.tap do |data| + if Feature.enabled?(:group_overview_security_dashboard) + data[:counts][:user_preferences] = user_preferences_usage + end + end end # rubocop: enable CodeReuse/ActiveRecord @@ -159,6 +165,10 @@ module Gitlab } end + def user_preferences_usage + {} # augmented in EE + end + def count(relation, fallback: -1) relation.count rescue ActiveRecord::StatementInvalid diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb index 5303b3582ab..e9be6db50da 100644 --- a/lib/gitlab/wiki_file_finder.rb +++ b/lib/gitlab/wiki_file_finder.rb @@ -2,8 +2,6 @@ module Gitlab class WikiFileFinder < FileFinder - BATCH_SIZE = 100 - attr_reader :repository def initialize(project, ref) @@ -19,7 +17,7 @@ module Gitlab safe_query = Regexp.new(safe_query, Regexp::IGNORECASE) filenames = repository.ls_files(ref) - filenames.grep(safe_query).first(BATCH_SIZE) + filenames.grep(safe_query) end end end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 4beb94eeb8e..b1db4dc94a6 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -10,6 +10,7 @@ namespace :dev do desc "GitLab | Eager load application" task load: :environment do + Rails.configuration.eager_load = true Rails.application.eager_load! end end diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index f71e69987cb..e876b23d43f 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -25,6 +25,11 @@ namespace :gitlab do puts "" end + # In production, we might want to prevent ourselves from shooting + # ourselves in the foot, so let's only do this in a test or + # development environment. + terminate_all_connections unless Rails.env.production? + Rake::Task["db:reset"].invoke Rake::Task["add_limits_mysql"].invoke Rake::Task["setup_postgresql"].invoke @@ -33,4 +38,24 @@ namespace :gitlab do puts "Quitting...".color(:red) exit 1 end + + # If there are any clients connected to the DB, PostgreSQL won't let + # you drop the database. It's possible that Sidekiq, Unicorn, or + # some other client will be hanging onto a connection, preventing + # the DROP DATABASE from working. To workaround this problem, this + # method terminates all the connections so that a subsequent DROP + # will work. + def self.terminate_all_connections + return false unless Gitlab::Database.postgresql? + + cmd = <<~SQL + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); + SQL + + ActiveRecord::Base.connection.execute(cmd)&.result_status == PG::PGRES_TUPLES_OK + rescue ActiveRecord::NoDatabaseError + end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8d7ed7a5b88..3c739759979 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -117,6 +117,9 @@ msgstr "" msgid "%{issuableType} will be removed! Are you sure?" msgstr "" +msgid "%{label_for_message} unavailable" +msgstr "" + msgid "%{link_start}Read more%{link_end} about role permissions" msgstr "" @@ -1323,6 +1326,9 @@ msgstr "" msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes." msgstr "" +msgid "Changes" +msgstr "" + msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision." msgstr "" @@ -1389,9 +1395,6 @@ msgstr "" msgid "Choose the top-level group for your repository imports." msgstr "" -msgid "Choose which repositories you want to import." -msgstr "" - msgid "CiStatusLabel|canceled" msgstr "" @@ -3459,7 +3462,7 @@ msgstr "" msgid "Found errors in your .gitlab-ci.yml:" msgstr "" -msgid "From %{provider_title}" +msgid "From %{providerTitle}" msgstr "" msgid "From Bitbucket" @@ -3486,9 +3489,15 @@ msgstr "" msgid "From the Kubernetes cluster details view, install Runner from the applications list" msgstr "" +msgid "GPG Key ID:" +msgstr "" + msgid "GPG Keys" msgstr "" +msgid "GPG signature (loading...)" +msgstr "" + msgid "General" msgstr "" @@ -3582,6 +3591,9 @@ msgstr "" msgid "Go to %{link_to_google_takeout}." msgstr "" +msgid "Go to project" +msgstr "" + msgid "Google Code import" msgstr "" @@ -3953,6 +3965,21 @@ msgstr "" msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds" msgstr "" +msgid "Import/Export illustration" +msgstr "" + +msgid "ImportProjects|Importing the project failed" +msgstr "" + +msgid "ImportProjects|Requesting your %{provider} repositories failed" +msgstr "" + +msgid "ImportProjects|Select the projects you want to import" +msgstr "" + +msgid "ImportProjects|Updating the imported projects failed" +msgstr "" + msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}." msgstr "" @@ -4046,6 +4073,9 @@ msgstr "" msgid "Introducing Your Conversational Development Index" msgstr "" +msgid "Invalid input, please avoid emojis" +msgstr "" + msgid "Invitation" msgstr "" @@ -4306,6 +4336,9 @@ msgstr "" msgid "Learn more about protected branches" msgstr "" +msgid "Learn more about signing commits" +msgstr "" + msgid "Learn more in the" msgstr "" @@ -4428,6 +4461,9 @@ msgstr "" msgid "Manifest file import" msgstr "" +msgid "Manual job" +msgstr "" + msgid "Map a FogBugz account ID to a GitLab user" msgstr "" @@ -4722,6 +4758,9 @@ msgstr "" msgid "More information is available|here" msgstr "" +msgid "More than %{number_commits_distance} commits different with %{default_branch}" +msgstr "" + msgid "Most stars" msgstr "" @@ -4859,6 +4898,9 @@ msgstr "" msgid "No" msgstr "" +msgid "No %{providerTitle} repositories available to import" +msgstr "" + msgid "No activities found" msgstr "" @@ -4967,6 +5009,9 @@ msgstr "" msgid "Not now" msgstr "" +msgid "Not started" +msgstr "" + msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}" msgstr "" @@ -5446,6 +5491,9 @@ msgstr "" msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again." msgstr "" +msgid "Please create a username with only alphanumeric characters." +msgstr "" + msgid "Please fill in a descriptive name for your group." msgstr "" @@ -5809,6 +5857,9 @@ msgstr "" msgid "Project export started. A download link will be sent by email." msgstr "" +msgid "Project has too many %{label_for_message} to search" +msgstr "" + msgid "Project members" msgstr "" @@ -6317,6 +6368,9 @@ msgstr "" msgid "Running" msgstr "" +msgid "Running…" +msgstr "" + msgid "SSH Keys" msgstr "" @@ -6359,6 +6413,9 @@ msgstr "" msgid "Schedules" msgstr "" +msgid "Scheduling" +msgstr "" + msgid "Scheduling Pipelines" msgstr "" @@ -7342,6 +7399,21 @@ msgstr "" msgid "This branch has changed since you started editing. Would you like to create a new branch?" msgstr "" +msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request." +msgstr "" + +msgid "This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user." +msgstr "" + +msgid "This commit was signed with a different user's verified signature." +msgstr "" + +msgid "This commit was signed with a verified signature, but the committer email is <strong>not verified</strong> to belong to the same user." +msgstr "" + +msgid "This commit was signed with an <strong>unverified</strong> signature." +msgstr "" + msgid "This container registry has been scheduled for deletion." msgstr "" @@ -7357,6 +7429,9 @@ msgstr "" msgid "This domain is not verified. You will need to verify ownership before access is enabled." msgstr "" +msgid "This field is required." +msgstr "" + msgid "This group" msgstr "" @@ -8594,6 +8669,9 @@ msgstr "" msgid "attach a new file" msgstr "" +msgid "authored" +msgstr "" + msgid "branch name" msgstr "" diff --git a/package.json b/package.json index c88ade87af6..21edd0dcf41 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "prettier-staged-save": "node ./scripts/frontend/prettier.js save", "prettier-all": "node ./scripts/frontend/prettier.js check-all", "prettier-all-save": "node ./scripts/frontend/prettier.js save-all", + "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.*", "webpack": "webpack --config config/webpack.config.js", "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, @@ -28,7 +29,7 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", - "@gitlab/svgs": "^1.51.0", + "@gitlab/svgs": "^1.52.0", "@gitlab/ui": "^2.0.2", "apollo-boost": "^0.1.20", "apollo-client": "^2.4.5", @@ -168,7 +169,11 @@ "karma-webpack": "^4.0.0-beta.0", "nodemon": "^1.18.9", "pixelmatch": "^4.0.2", + "postcss": "^7.0.14", "prettier": "1.16.1", + "stylelint": "^9.10.1", + "stylelint-config-recommended": "^2.1.0", + "stylelint-scss": "^3.5.3", "vue-jest": "^3.0.2", "webpack-dev-server": "^3.1.14", "yarn-deduplicate": "^1.1.0" diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb index 6d1601dfa48..e0fbbed2567 100644 --- a/qa/qa/ce/strategy.rb +++ b/qa/qa/ce/strategy.rb @@ -8,7 +8,10 @@ module QA end def perform_before_hooks - # noop + # The login page could take some time to load the first time it is visited. + # We visit the login page and wait for it to properly load only once before the tests. + QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login) + QA::Page::Main::Login.perform(&:assert_page_loaded) end end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 69a8e691ceb..01ac161d26d 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -24,7 +24,7 @@ module QA end end - def with_retry(max_attempts: 3, reload: false) + def retry_until(max_attempts: 3, reload: false) attempts = 0 while attempts < max_attempts @@ -39,6 +39,21 @@ module QA false end + def retry_on_exception(max_attempts: 3, reload: false, sleep_interval: 0.0) + attempts = 0 + + begin + yield + rescue StandardError + sleep sleep_interval + refresh if reload + attempts += 1 + + retry if attempts < max_attempts + raise + end + end + def scroll_to(selector, text: nil) page.execute_script <<~JS var elements = Array.from(document.querySelectorAll('#{selector}')); diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 6dd9ff997a4..9d6bd338027 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -45,15 +45,17 @@ module QA private def select_kind(kind) - within_element(:new_project_or_subgroup_dropdown) do - # May need to click again because it is possible to click the button quicker than the JS is bound - wait(reload: false) do - click_element :new_project_or_subgroup_dropdown_toggle + retry_on_exception(sleep_interval: 1.0) do + within_element(:new_project_or_subgroup_dropdown) do + # May need to click again because it is possible to click the button quicker than the JS is bound + wait(reload: false) do + click_element :new_project_or_subgroup_dropdown_toggle - has_element?(kind) - end + has_element?(kind) + end - click_element kind + click_element kind + end end end end diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb index f0d323ca3b4..de0cfa9f293 100644 --- a/qa/qa/page/label/index.rb +++ b/qa/qa/page/label/index.rb @@ -14,6 +14,10 @@ module QA element :label_svg end + view 'app/views/shared/empty_states/_priority_labels.html.haml' do + element :label_svg + end + def go_to_new_label # The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit # This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?) diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index e476cbe29a2..e03fe9ab83a 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -40,6 +40,12 @@ module QA element :login_page end + def assert_page_loaded + unless page_loaded? + raise QA::Runtime::Browser::NotRespondingError, "Login page did not load at #{QA::Page::Main::Login.perform(&:current_url)}" + end + end + def page_loaded? wait(max: 60) do has_element?(:login_page) diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 616d50f47fc..55500e831c6 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -57,7 +57,7 @@ module QA end def go_to_profile_settings - with_retry(reload: false) do + retry_until(reload: false) do within_user_menu do click_link 'Settings' end diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb index 9ca498012eb..46a105003d0 100644 --- a/qa/qa/page/main/sign_up.rb +++ b/qa/qa/page/main/sign_up.rb @@ -23,7 +23,7 @@ module QA check_element :new_user_accept_terms if has_element?(:new_user_accept_terms) - signed_in = with_retry do + signed_in = retry_until do click_element :new_user_register_button Page::Main::Menu.act { has_personal_area? } diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb index a3cde73d3f2..488157d9878 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -10,12 +10,11 @@ module QA element :list_repos_button, "submit_tag _('List your GitHub repositories')" # rubocop:disable QA/ElementWithPattern end - view 'app/views/import/_githubish_status.html.haml' do - element :project_import_row, 'data: { qa: { repo_path: repo.full_name } }' # rubocop:disable QA/ElementWithPattern + view 'app/assets/javascripts/import_projects/components/provider_repo_table_row.vue' do + element :project_import_row element :project_namespace_select - element :project_namespace_field, 'select_tag :namespace_id' # rubocop:disable QA/ElementWithPattern - element :project_path_field, 'text_field_tag :path, sanitize_project_name(repo.name)' # rubocop:disable QA/ElementWithPattern - element :import_button, "_('Import')" # rubocop:disable QA/ElementWithPattern + element :project_path_field + element :import_button end def add_personal_access_token(personal_access_token) diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index a3e126b51da..2b6c01888d5 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -80,7 +80,7 @@ module QA # Retry the attempt to click :commit_button just in case part of the # animation is still in process even when the buttons have the # expected visibility. - commit_success_msg_shown = with_retry do + commit_success_msg_shown = retry_until do click_element :commit_button wait(reload: false) do diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 0bcf5e693f0..0b805b855ac 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -8,6 +8,8 @@ module QA class Browser include QA::Scenario::Actable + NotRespondingError = Class.new(RuntimeError) + def initialize self.class.configure! end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index 2fb8402edd8..6632c2977ef 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/72 - context 'Manage', :smoke, :quarantine do + context 'Manage', :smoke do describe 'Project creation' do it 'user creates a new project' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 3e7a6dd26ee..20a153f3f63 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -5,40 +5,16 @@ require_relative '../qa' end RSpec.configure do |config| - ServerNotRespondingError = Class.new(RuntimeError) - - # The login page could take some time to load the first time it is visited. - # We visit the login page and wait for it to properly load only once at the beginning of the suite. - config.before(:suite) do - if QA::Runtime::Scenario.respond_to?(:gitlab_address) - QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login) - - unless QA::Page::Main::Login.perform(&:page_loaded?) - raise ServerNotRespondingError, "Login page did not load at #{QA::Page::Main::Login.perform(&:current_url)}" - end + config.before(:context) do + if self.class.metadata.keys.include?(:quarantine) + skip_or_run_quarantined_tests(self.class.metadata.keys, config.inclusion_filter.rules.keys) end end config.before do |example| QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug? - # If quarantine is tagged, skip tests that have other metadata unless - # they're also tagged. This lets us run quarantined tests in a particular - # category without running tests in other categories. - # E.g., if a test is tagged 'smoke' and 'quarantine', and another is tagged - # 'ldap' and 'quarantine', if we wanted to run just quarantined smoke tests - # using `--tag quarantine --tag smoke`, without this check we'd end up - # running that ldap test as well. - if config.inclusion_filter[:quarantine] - skip("Running tests tagged with all of #{config.inclusion_filter.rules.keys}") unless quarantine_and_optional_other_tag?(example, config) - end - end - - config.before(:each, :quarantine) do |example| - # Skip tests in quarantine unless we explicitly focus on them - # We could use an exclusion filter, but this way the test report will list - # the quarantined tests when they're not run so that we're aware of them - skip('In quarantine') unless config.inclusion_filter[:quarantine] + skip_or_run_quarantined_tests(example.metadata.keys, config.inclusion_filter.rules.keys) end config.expect_with :rspec do |expectations| @@ -57,18 +33,41 @@ RSpec.configure do |config| Kernel.srand config.seed end +# Skip tests in quarantine unless we explicitly focus on them. +# Skip the entire context if a context is tagged. This avoids running before +# blocks unnecessarily. +# If quarantine is focussed, skip tests/contexts that have other metadata +# unless they're also focussed. This lets us run quarantined tests in a +# particular category without running tests in other categories. +# E.g., if a test is tagged 'smoke' and 'quarantine', and another is tagged +# 'ldap' and 'quarantine', if we wanted to run just quarantined smoke tests +# using `--tag quarantine --tag smoke`, without this check we'd end up +# running that ldap test as well. +# We could use an exclusion filter, but this way the test report will list +# the quarantined tests when they're not run so that we're aware of them +def skip_or_run_quarantined_tests(metadata_keys, filter_keys) + included_filters = filters_other_than_quarantine(filter_keys) + + if filter_keys.include?(:quarantine) + skip("Only running tests tagged with :quarantine and any of #{included_filters}") unless quarantine_and_optional_other_tag?(metadata_keys, included_filters) + else + skip('In quarantine') if metadata_keys.include?(:quarantine) + end +end + +def filters_other_than_quarantine(filter_keys) + filter_keys.reject { |key| key == :quarantine } +end + # Checks if a test has the 'quarantine' tag and other tags in the inclusion filter. # # Returns true if -# - the example metadata includes the quarantine tag -# - and the metadata and inclusion filter both have any other tag -# - or no other tags are in the inclusion filter -def quarantine_and_optional_other_tag?(example, config) - return false unless example.metadata.keys.include? :quarantine - - filters_other_than_quarantine = config.inclusion_filter.rules.keys.reject { |key| key == :quarantine } - - return true if filters_other_than_quarantine.empty? +# - the metadata includes the quarantine tag +# - and the metadata and inclusion filter both have any other tag +# - or no other tags are in the inclusion filter +def quarantine_and_optional_other_tag?(metadata_keys, included_filters) + return false unless metadata_keys.include? :quarantine + return true if included_filters.empty? - filters_other_than_quarantine.any? { |key| example.metadata.keys.include? key } + included_filters.any? { |key| metadata_keys.include? key } end diff --git a/qa/spec/spec_helper_spec.rb b/qa/spec/spec_helper_spec.rb index f001200fb52..2427999e110 100644 --- a/qa/spec/spec_helper_spec.rb +++ b/qa/spec/spec_helper_spec.rb @@ -10,79 +10,79 @@ describe 'rspec config tests' do end end + context 'default' do + it_behaves_like 'passing tests' + end + context 'foo', :foo do it_behaves_like 'passing tests' end - context 'default' do + context 'quarantine', :quarantine do + it_behaves_like 'passing tests' + end + + context 'bar quarantine', :bar, :quarantine do it_behaves_like 'passing tests' end end end - context 'default config' do - it 'tests are skipped if in quarantine' do + context 'with no tags focussed' do + before do group.run + end - foo_context = group.children.find { |c| c.description == "foo" } - foo_examples = foo_context.descendant_filtered_examples - expect(foo_examples.count).to eq(2) - - ex = foo_examples.find { |e| e.description == "not in quarantine" } - expect(ex.execution_result.status).to eq(:passed) + context 'in a context tagged :foo' do + it 'skips tests in quarantine' do + context = group.children.find { |c| c.description == "foo" } + examples = context.descendant_filtered_examples + expect(examples.count).to eq(2) - ex = foo_examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('In quarantine') + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) - default_context = group.children.find { |c| c.description == "default" } - default_examples = default_context.descendant_filtered_examples - expect(default_examples.count).to eq(2) + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + end + end - ex = default_examples.find { |e| e.description == "not in quarantine" } - expect(ex.execution_result.status).to eq(:passed) + context 'in an untagged context' do + it 'skips tests in quarantine' do + context = group.children.find { |c| c.description == "default" } + examples = context.descendant_filtered_examples + expect(examples.count).to eq(2) - ex = default_examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('In quarantine') - end - end + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) - context "with 'quarantine' tagged" do - before do - RSpec.configure do |config| - config.inclusion_filter = :quarantine - end - end - after do - RSpec.configure do |config| - config.inclusion_filter.clear + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') end end - it "only quarantined tests are run" do - group.run - - foo_context = group.children.find { |c| c.description == "foo" } - foo_examples = foo_context.descendant_filtered_examples - expect(foo_examples.count).to be(1) - - ex = foo_examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to eq(:passed) + context 'in a context tagged :quarantine' do + it 'skips all tests' do + context = group.children.find { |c| c.description == "quarantine" } + examples = context.descendant_filtered_examples + expect(examples.count).to eq(2) - default_context = group.children.find { |c| c.description == "default" } - default_examples = default_context.descendant_filtered_examples - expect(default_examples.count).to be(1) + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:pending) - ex = default_examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to eq(:passed) + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + end end end - context "with 'foo' tagged" do + context 'with :quarantine focussed' do before do RSpec.configure do |config| - config.inclusion_filter = :foo + config.inclusion_filter = :quarantine end group.run @@ -93,30 +93,50 @@ describe 'rspec config tests' do end end - it "tests are not run if not tagged 'foo'" do - default_context = group.children.find { |c| c.description == "default" } - expect(default_context.descendant_filtered_examples.count).to eq(0) + context 'in an untagged context' do + it 'only runs quarantined tests' do + context = group.children.find { |c| c.description == "default" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(1) + + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + end end - it "tests are skipped if in quarantine" do - foo_context = group.children.find { |c| c.description == "foo" } - foo_examples = foo_context.descendant_filtered_examples - expect(foo_examples.count).to eq(2) + context 'in a context tagged :foo' do + it 'only runs quarantined tests' do + context = group.children.find { |c| c.description == "foo" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(1) - ex = foo_examples.find { |e| e.description == "not in quarantine" } - expect(ex.execution_result.status).to eq(:passed) + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + end + end + + context 'in a context tagged :quarantine' do + it 'runs all tests' do + context = group.children.find { |c| c.description == "quarantine" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) + + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) - ex = foo_examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('In quarantine') + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + end end end - context "with 'quarantine' and 'foo' tagged" do + context 'with a non-quarantine tag (:foo) focussed' do before do RSpec.configure do |config| - config.inclusion_filter = { quarantine: true, foo: true } + config.inclusion_filter = :foo end + + group.run end after do RSpec.configure do |config| @@ -124,38 +144,43 @@ describe 'rspec config tests' do end end - it 'of tests tagged foo, only tests in quarantine run' do - group.run + context 'in an untagged context' do + it 'runs no tests' do + context = group.children.find { |c| c.description == "default" } + expect(context.descendant_filtered_examples.count).to eq(0) + end + end - foo_context = group.children.find { |c| c.description == "foo" } - foo_examples = foo_context.descendant_filtered_examples - expect(foo_examples.count).to eq(2) + context 'in a context tagged :foo' do + it 'skips quarantined tests' do + context = group.children.find { |c| c.description == "foo" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) - ex = foo_examples.find { |e| e.description == "not in quarantine" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:quarantine, :foo]') + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) - ex = foo_examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to eq(:passed) + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + end end - it 'if tests are not tagged they are skipped, even if they are in quarantine' do - group.run - default_context = group.children.find { |c| c.description == "default" } - default_examples = default_context.descendant_filtered_examples - expect(default_examples.count).to eq(1) - - ex = default_examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:quarantine, :foo]') + context 'in a context tagged :quarantine' do + it 'runs no tests' do + context = group.children.find { |c| c.description == "quarantine" } + expect(context.descendant_filtered_examples.count).to eq(0) + end end end - context "with 'foo' and 'bar' tagged" do + context 'with :quarantine and a non-quarantine tag (:foo) focussed' do before do RSpec.configure do |config| - config.inclusion_filter = { bar: true, foo: true } + config.inclusion_filter = { quarantine: true, foo: true } end + + group.run end after do RSpec.configure do |config| @@ -163,67 +188,67 @@ describe 'rspec config tests' do end end - it "runs tests tagged either 'foo' or 'bar'" do - group = RSpec.describe do - example 'foo', :foo do - end - example 'bar', :bar do - end - example 'foo and bar', :foo, :bar do - end - end - - group.run - expect(group.examples.count).to eq(3) + context 'in an untagged context' do + it 'ignores untagged tests and skips tests even if in quarantine' do + context = group.children.find { |c| c.description == "default" } + examples = context.descendant_filtered_examples + expect(examples.count).to eq(1) - ex = group.examples.find { |e| e.description == "foo" } - expect(ex.execution_result.status).to eq(:passed) + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + end + end - ex = group.examples.find { |e| e.description == "bar" } - expect(ex.execution_result.status).to eq(:passed) + context 'in a context tagged :foo' do + it 'only runs quarantined tests' do + context = group.children.find { |c| c.description == "foo" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) - ex = group.examples.find { |e| e.description == "foo and bar" } - expect(ex.execution_result.status).to eq(:passed) - end + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) - it "skips quarantined tests tagged 'foo' and/or 'bar'" do - group = RSpec.describe do - example 'foo in quarantine', :foo, :quarantine do - end - example 'foo and bar in quarantine', :foo, :bar, :quarantine do - end + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:pending) end + end - group.run - expect(group.examples.count).to eq(2) + context 'in a context tagged :quarantine' do + it 'skips all tests' do + context = group.children.find { |c| c.description == "quarantine" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) - ex = group.examples.find { |e| e.description == "foo in quarantine" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('In quarantine') + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) - ex = group.examples.find { |e| e.description == "foo and bar in quarantine" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('In quarantine') + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + end end - it "ignores quarantined tests not tagged either 'foo' or 'bar'" do - group = RSpec.describe do - example 'in quarantine', :quarantine do - end - end + context 'in a context tagged :bar and :quarantine' do + it 'skips all tests' do + context = group.children.find { |c| c.description == "quarantine" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) - group.run + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) - ex = group.examples.find { |e| e.description == "in quarantine" } - expect(ex.execution_result.status).to be_nil + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + end end end - context "with 'foo' and 'bar' and 'quarantined' tagged" do + context 'with :quarantine and multiple non-quarantine tags focussed' do before do RSpec.configure do |config| config.inclusion_filter = { bar: true, foo: true, quarantine: true } end + + group.run end after do RSpec.configure do |config| @@ -231,34 +256,49 @@ describe 'rspec config tests' do end end - it "runs tests tagged 'quarantine' and 'foo' or 'bar'" do - group = RSpec.describe do - example 'foo', :foo do - end - example 'bar and quarantine', :bar, :quarantine do - end - example 'foo and bar', :foo, :bar do - end - example 'foo, bar, and quarantine', :foo, :bar, :quarantine do - end + context 'in a context tagged :foo' do + it 'only runs quarantined tests' do + context = group.children.find { |c| c.description == "foo" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) + + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('Only running tests tagged with :quarantine and any of [:bar, :foo]') end + end - group.run - expect(group.examples.count).to eq(4) + context 'in a context tagged :quarantine' do + it 'skips all tests' do + context = group.children.find { |c| c.description == "quarantine" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) - ex = group.examples.find { |e| e.description == "foo" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:bar, :foo, :quarantine]') + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('Only running tests tagged with :quarantine and any of [:bar, :foo]') - ex = group.examples.find { |e| e.description == "bar and quarantine" } - expect(ex.execution_result.status).to eq(:passed) + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('Only running tests tagged with :quarantine and any of [:bar, :foo]') + end + end - ex = group.examples.find { |e| e.description == "foo and bar" } - expect(ex.execution_result.status).to eq(:pending) - expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:bar, :foo, :quarantine]') + context 'in a context tagged :bar and :quarantine' do + it 'runs all tests' do + context = group.children.find { |c| c.description == "bar quarantine" } + examples = context.descendant_filtered_examples + expect(examples.count).to be(2) - ex = group.examples.find { |e| e.description == "foo, bar, and quarantine" } - expect(ex.execution_result.status).to eq(:passed) + ex = examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + + ex = examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + end end end end diff --git a/scripts/lint-changelog-yaml b/scripts/lint-changelog-yaml index 6553e02ffca..06d502c4676 100755 --- a/scripts/lint-changelog-yaml +++ b/scripts/lint-changelog-yaml @@ -3,7 +3,7 @@ require 'yaml' invalid_changelogs = Dir['changelogs/**/*'].reject do |changelog| - next true if changelog =~ /(archive\.md|unreleased(-ee)?)$/ + next true if changelog =~ /((README|archive)\.md|unreleased(-ee)?)$/ next false unless changelog.end_with?('.yml') begin diff --git a/scripts/security-harness b/scripts/security-harness index c60b3410095..8369cf06223 100755 --- a/scripts/security-harness +++ b/scripts/security-harness @@ -37,7 +37,7 @@ else end __END__ -#!/bin/sh +#!/bin/bash set -e diff --git a/scripts/static-analysis b/scripts/static-analysis index 25ba7ec6c8e..642c50ec0a8 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -29,6 +29,7 @@ tasks = [ %w[bin/rake lint:all], %w[bundle exec license_finder], %w[yarn run eslint], + %w[yarn run stylelint], %w[yarn run prettier-all], %w[bundle exec rubocop --parallel], %w[scripts/lint-conflicts.sh], diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb new file mode 100644 index 00000000000..01ed81964c3 --- /dev/null +++ b/spec/config/application_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Application do # rubocop:disable RSpec/FilePath + using RSpec::Parameterized::TableSyntax + + FILTERED_PARAM = ActionDispatch::Http::ParameterFilter::FILTERED + + context 'when parameters are logged' do + describe 'rails does not leak confidential parameters' do + def request_for_url(input_url) + env = Rack::MockRequest.env_for(input_url) + env['action_dispatch.parameter_filter'] = described_class.config.filter_parameters + + ActionDispatch::Request.new(env) + end + + where(:input_url, :output_query) do + '/' | {} + '/?safe=1' | { 'safe' => '1' } + '/?private_token=secret' | { 'private_token' => FILTERED_PARAM } + '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => FILTERED_PARAM } + '/?note=secret¬eable=1&prefix_note=2' | { 'note' => FILTERED_PARAM, 'noteable' => '1', 'prefix_note' => '2' } + '/?note[note]=secret&target_type=1' | { 'note' => FILTERED_PARAM, 'target_type' => '1' } + '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => FILTERED_PARAM }, 'target_type' => '1' } + end + + with_them do + it { expect(request_for_url(input_url).filtered_parameters).to eq(output_query) } + end + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 6b66cbd2651..c934db9e237 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -8,6 +8,17 @@ describe Admin::UsersController do sign_in(admin) end + describe 'GET :id' do + it 'finds a user case-insensitively' do + user = create(:user, username: 'CaseSensitive') + + get :show, params: { id: user.username.downcase } + + expect(response).to be_redirect + expect(response.location).to end_with(user.username) + end + end + describe 'DELETE #user with projects' do let(:project) { create(:project, namespace: user.namespace) } let!(:issue) { create(:issue, author: user) } diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index a07113a6156..cf3b24f50a3 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -52,6 +52,23 @@ describe SendFileUpload do end end + context 'with inline image' do + let(:filename) { 'test.png' } + let(:params) { { disposition: 'inline', attachment: filename } } + + it 'sends a file with inline disposition' do + # Notice the filename= is omitted from the disposition; this is because + # Rails 5 will append this header in send_file + expected_params = { + filename: 'test.png', + disposition: "inline; filename*=UTF-8''test.png" + } + expect(controller).to receive(:send_file).with(uploader.path, expected_params) + + subject + end + end + context 'with attachment' do let(:filename) { 'test.js' } let(:params) { { attachment: filename } } diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 8b176e07bc8..4b164d0aa6b 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -3,11 +3,9 @@ require 'spec_helper' describe Dashboard::MilestonesController do let(:project) { create(:project) } let(:group) { create(:group) } - let(:public_group) { create(:group, :public) } let(:user) { create(:user) } let(:project_milestone) { create(:milestone, project: project) } let(:group_milestone) { create(:milestone, group: group) } - let!(:public_milestone) { create(:milestone, group: public_group) } let(:milestone) do DashboardMilestone.build( [project], @@ -45,6 +43,9 @@ describe Dashboard::MilestonesController do end describe "#index" do + let(:public_group) { create(:group, :public) } + let!(:public_milestone) { create(:milestone, group: public_group) } + render_views it 'returns group and project milestones to which the user belongs' do @@ -74,10 +75,10 @@ describe Dashboard::MilestonesController do expect(response.body).not_to include(project_milestone.title) end - it 'should contain group and project milestones to which the user belongs to' do + it 'should show counts of group and project milestones to which the user belongs to' do get :index - expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>") + expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>") expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") end end diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 360030102e0..ef23ffaa843 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -453,7 +453,7 @@ describe Groups::ClustersController do end context 'when domain is invalid' do - let(:domain) { 'not-a-valid-domain' } + let(:domain) { 'http://not-a-valid-domain' } it 'should not update cluster attributes' do go diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 5cb284e7e2d..dca67c18caa 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -37,6 +37,46 @@ describe HelpController do expect(assigns[:help_index]).to eq '[external](https://some.external.link)' end end + + context 'when relative url with external on same line' do + it 'prefix it with /help/' do + stub_readme("[API](api/README.md) [external](https://some.external.link)") + + get :index + + expect(assigns[:help_index]).to eq '[API](/help/api/README.md) [external](https://some.external.link)' + end + end + + context 'when relative url with http:// in query' do + it 'prefix it with /help/' do + stub_readme("[API](api/README.md?go=https://example.com/)") + + get :index + + expect(assigns[:help_index]).to eq '[API](/help/api/README.md?go=https://example.com/)' + end + end + + context 'when mailto URL' do + it 'do not change it' do + stub_readme("[report bug](mailto:bugs@example.com)") + + get :index + + expect(assigns[:help_index]).to eq '[report bug](mailto:bugs@example.com)' + end + end + + context 'when protocol-relative link' do + it 'do not change it' do + stub_readme("[protocol-relative](//example.com)") + + get :index + + expect(assigns[:help_index]).to eq '[protocol-relative](//example.com)' + end + end end describe 'GET #show' do diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 5ba64ab3eed..8cbec79095f 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -40,4 +40,12 @@ describe Import::GiteaController do end end end + + describe "GET realtime_changes" do + it_behaves_like 'a GitHub-ish import controller: GET realtime_changes' do + before do + assign_host_url + end + end + end end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index bca5f3f6589..162dff98ec5 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -60,4 +60,8 @@ describe Import::GithubController do describe "POST create" do it_behaves_like 'a GitHub-ish import controller: POST create' end + + describe "GET realtime_changes" do + it_behaves_like 'a GitHub-ish import controller: GET realtime_changes' + end end diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index a6017d8e5e6..e85f32d6e30 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -4,10 +4,11 @@ describe Projects::MergeRequests::DiffsController do include ProjectForksHelper let(:project) { create(:project, :repository) } - let(:user) { project.owner } + let(:user) { create(:user) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } before do + project.add_maintainer(user) sign_in(user) end @@ -114,16 +115,6 @@ describe Projects::MergeRequests::DiffsController do expect(paths).to include(existing_path) end end - - context 'when the path does not exist in the diff' do - before do - diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') - end - - it 'returns a 404' do - expect(response).to have_gitlab_http_status(404) - end - end end context 'when the user cannot view the merge request' do diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb index 8b7f7587701..ffb9867a203 100644 --- a/spec/controllers/projects/pages_domains_controller_spec.rb +++ b/spec/controllers/projects/pages_domains_controller_spec.rb @@ -23,12 +23,27 @@ describe Projects::PagesDomainsController do end describe 'GET show' do - it "displays the 'show' page" do + def make_request get(:show, params: request_params.merge(id: pages_domain.domain)) + end + it "displays the 'show' page" do + make_request expect(response).to have_gitlab_http_status(200) expect(response).to render_template('show') end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + it 'renders 404 page' do + make_request + + expect(response).to have_gitlab_http_status(404) + end + end end describe 'GET new' do diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 51f158d3045..fd8677feab5 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -126,7 +126,7 @@ describe 'Dashboard Todos' do it 'shows you added a todo message' do page.within('.js-todos-all') do - expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") + expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end @@ -140,7 +140,7 @@ describe 'Dashboard Todos' do it 'shows you mentioned yourself message' do page.within('.js-todos-all') do - expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") + expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end @@ -154,7 +154,7 @@ describe 'Dashboard Todos' do it 'shows you directly addressed yourself message' do page.within('.js-todos-all') do - expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") + expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end @@ -170,7 +170,7 @@ describe 'Dashboard Todos' do it 'shows you set yourself as an approver message' do page.within('.js-todos-all') do - expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") + expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end diff --git a/spec/features/groups/labels/create_spec.rb b/spec/features/groups/labels/create_spec.rb new file mode 100644 index 00000000000..f5062a65321 --- /dev/null +++ b/spec/features/groups/labels/create_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Create a group label' do + let(:user) { create(:user) } + let(:group) { create(:group) } + + before do + group.add_owner(user) + sign_in(user) + visit group_labels_path(group) + end + + it 'creates a new label' do + click_link 'New label' + fill_in 'Title', with: 'test-label' + click_button 'Create label' + + expect(page).to have_content 'test-label' + expect(current_path).to eq(group_labels_path(group)) + end +end diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb index 0ce7dad4040..62308d3b518 100644 --- a/spec/features/groups/labels/index_spec.rb +++ b/spec/features/groups/labels/index_spec.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' describe 'Group labels' do let(:user) { create(:user) } let(:group) { create(:group) } let!(:label) { create(:group_label, group: group) } + let!(:label2) { create(:group_label) } before do group.add_owner(user) @@ -11,7 +14,16 @@ describe 'Group labels' do visit group_labels_path(group) end - it 'label has edit button', :js do + it 'shows labels that belong to the group' do + expect(page).to have_content(label.name) + expect(page).not_to have_content(label2.name) + end + + it 'shows a new label button' do + expect(page).to have_link('New label') + end + + it 'shows an edit label button', :js do expect(page).to have_selector('.label-action.edit') end end diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index d19408ee87f..c837a6752f9 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -222,6 +222,11 @@ describe 'Merge request > User creates image diff notes', :js do end def create_image_diff_note + expand_text = 'Click to expand it.' + page.all('a', text: expand_text).each do |element| + element.click + end + find('.js-add-image-diff-note-button', match: :first).click find('.diff-content .note-textarea').native.send_keys('image diff test comment') click_button 'Comment' diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index 38b4e4a6d1b..ea2bb1503bb 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -8,6 +8,8 @@ describe "User creates a merge request", :js do let(:user) { create(:user) } before do + stub_feature_flags(approval_rules: false) + project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb index 62ae793151c..777d30fdffd 100644 --- a/spec/features/projects/branches/user_views_branches_spec.rb +++ b/spec/features/projects/branches/user_views_branches_spec.rb @@ -15,6 +15,8 @@ describe "User views branches" do it "shows branches" do expect(page).to have_content("Branches").and have_content("master") + + expect(page.all(".graph-side")).to all( have_content(/\d+/) ) end end diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb index df33d215602..dc0278370aa 100644 --- a/spec/features/projects/settings/forked_project_settings_spec.rb +++ b/spec/features/projects/settings/forked_project_settings_spec.rb @@ -7,6 +7,7 @@ describe 'Projects > Settings > For a forked project', :js do let(:forked_project) { fork_project(original_project, user) } before do + stub_feature_flags(approval_rules: false) original_project.add_maintainer(user) forked_project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 3b469fee867..49244c53a91 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe 'Projects > Wiki > User previews markdown changes', :js do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) } let(:wiki_content) do <<-HEREDOC [regular link](regular) @@ -18,9 +19,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do sign_in(user) - visit project_path(project) - find('.shortcuts-wiki').click - click_link "Create your first page" + visit project_wiki_path(project, wiki_page) end context "while creating a new wiki page" do diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 48a0d675f2d..b1a7f167977 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -44,13 +44,7 @@ describe "User creates wiki page" do end it "shows non-escaped link in the pages list", :js do - click_link("New page") - - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "one/two/three-test") - - click_on("Create page") - end + fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do fill_in(:wiki_content, with: "wiki content") diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index f76e577b0d6..dbf8af3e5bb 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -26,12 +26,7 @@ describe 'User updates wiki page' do end it 'updates a page that has a path', :js do - click_on('New page') - - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + fill_in(:wiki_title, with: 'one/two/three-test') page.within '.wiki-form' do fill_in(:wiki_content, with: 'wiki content') diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index d4691b669c1..6e28ec0d7b2 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -22,12 +22,7 @@ describe 'User views a wiki page' do visit(project_wikis_path(project)) click_link "Create your first page" - click_on('New page') - - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + fill_in(:wiki_title, with: 'one/two/three-test') page.within('.wiki-form') do fill_in(:wiki_content, with: 'wiki content') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index bc36c6f948f..dbf0d427976 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -4,6 +4,10 @@ describe 'Project' do include ProjectForksHelper include MobileHelpers + before do + stub_feature_flags(approval_rules: false) + end + describe 'creating from template' do let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index bfe11ddf673..957c3cfc583 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -49,6 +49,34 @@ describe 'Signup' do expect(page).to have_content("Please create a username with only alphanumeric characters.") end + + it 'shows an error border if the username contains emojis' do + simulate_input('#new_user_username', 'ehsan😀') + + expect(find('.username')).to have_css '.gl-field-error-outline' + end + + it 'shows an error message if the username contains emojis' do + simulate_input('#new_user_username', 'ehsan😀') + + expect(page).to have_content("Invalid input, please avoid emojis") + end + end + + describe 'user\'s full name validation', :js do + before do + visit root_path + click_link 'Register' + simulate_input('#new_user_name', 'Ehsan 🦋') + end + + it 'shows an error border if the user\'s fullname contains an emoji' do + expect(find('.name')).to have_css '.gl-field-error-outline' + end + + it 'shows an error message if the username contains emojis' do + expect(page).to have_content("Invalid input, please avoid emojis") + end end context 'with no errors' do diff --git a/spec/fixtures/api/schemas/entities/diff_viewer.json b/spec/fixtures/api/schemas/entities/diff_viewer.json index 81325cd86c6..ae0fb32d3ac 100644 --- a/spec/fixtures/api/schemas/entities/diff_viewer.json +++ b/spec/fixtures/api/schemas/entities/diff_viewer.json @@ -14,6 +14,17 @@ "string", "null" ] + }, + "error_message": { + "type": [ + "string", + "null" + ] + }, + "collapsed": { + "type": [ + "boolean" + ] } }, "additionalProperties": false diff --git a/spec/fixtures/api/schemas/public_api/v4/group_labels.json b/spec/fixtures/api/schemas/public_api/v4/group_labels.json index f6c327abfdd..fbde45f2904 100644 --- a/spec/fixtures/api/schemas/public_api/v4/group_labels.json +++ b/spec/fixtures/api/schemas/public_api/v4/group_labels.json @@ -6,6 +6,7 @@ "id" : { "type": "integer" }, "name" : { "type": "string "}, "color" : { "type": "string "}, + "text_color" : { "type": "string "}, "description" : { "type": "string "}, "open_issues_count" : { "type": "integer "}, "closed_issues_count" : { "type": "integer "}, diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json index 68c6099836b..03dfc647162 100644 --- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json +++ b/spec/fixtures/security-reports/master/gl-container-scanning-report.json @@ -1,11 +1,14 @@ { "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff", "unapproved": [ - "CVE-2017-18018", - "CVE-2016-2781", - "CVE-2017-12424", - "CVE-2007-5686", - "CVE-2013-4235" + "CVE-2017-18269", + "CVE-2017-16997", + "CVE-2018-1000001", + "CVE-2016-10228", + "CVE-2018-18520", + "CVE-2010-4052", + "CVE-2018-16869", + "CVE-2018-18311" ], "vulnerabilities": [ { @@ -87,6 +90,16 @@ "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311", "severity": "Unknown", "fixedby": "5.24.1-3+deb9u5" + }, + { + "featurename": "foo", + "featureversion": "1.3", + "vulnerability": "CVE-2018-666", + "namespace": "debian:9", + "description": "Foo has a vulnerability nobody cares about and whitelist.", + "link": "https://security-tracker.debian.org/tracker/CVE-2018-666", + "severity": "Unknown", + "fixedby": "1.4" } ] } diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb new file mode 100644 index 00000000000..e3a34762b62 --- /dev/null +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::BaseResolver do + include GraphqlHelpers + + let(:resolver) do + Class.new(described_class) do + def resolve(**args) + [args, args] + end + end + end + + describe '.single' do + it 'returns a subclass from the resolver' do + expect(resolver.single.superclass).to eq(resolver) + end + + it 'returns the same subclass every time' do + expect(resolver.single.object_id).to eq(resolver.single.object_id) + end + + it 'returns a resolver that gives the first result from the original resolver' do + result = resolve(resolver.single, args: { test: 1 }) + + expect(result).to eq(test: 1) + end + end +end diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 1a54ab540fc..66de372e9fe 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -33,6 +33,10 @@ describe Resolvers::IssuesResolver do expect(resolve_issues).to contain_exactly(issue, issue2) end + it 'finds a specific issue with iid' do + expect(resolve_issues(iid: issue.iid)).to contain_exactly(issue) + end + it 'finds a specific issue with iids' do expect(resolve_issues(iids: issue.iid)).to contain_exactly(issue) end diff --git a/spec/graphql/resolvers/merge_request_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index 73993b3a039..ab3c426b2cd 100644 --- a/spec/graphql/resolvers/merge_request_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Resolvers::MergeRequestResolver do +describe Resolvers::MergeRequestsResolver do include GraphqlHelpers set(:project) { create(:project, :repository) } @@ -16,9 +16,17 @@ describe Resolvers::MergeRequestResolver do let(:other_iid) { other_merge_request.iid } describe '#resolve' do - it 'batch-resolves merge requests by target project full path and IID' do + it 'batch-resolves by target project full path and individual IID' do result = batch(max_queries: 2) do - [resolve_mr(project, iid_1), resolve_mr(project, iid_2)] + resolve_mr(project, iid: iid_1) + resolve_mr(project, iid: iid_2) + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2) + end + + it 'batch-resolves by target project full path and IIDS' do + result = batch(max_queries: 2) do + resolve_mr(project, iids: [iid_1, iid_2]) end expect(result).to contain_exactly(merge_request_1, merge_request_2) @@ -26,20 +34,28 @@ describe Resolvers::MergeRequestResolver do it 'can batch-resolve merge requests from different projects' do result = batch(max_queries: 3) do - [resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)] + resolve_mr(project, iid: iid_1) + + resolve_mr(project, iid: iid_2) + + resolve_mr(other_project, iid: other_iid) end expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) end - it 'resolves an unknown iid to nil' do - result = batch { resolve_mr(project, -1) } + it 'resolves an unknown iid to be empty' do + result = batch { resolve_mr(project, iid: -1) } + + expect(result).to be_empty + end + + it 'resolves empty iids to be empty' do + result = batch { resolve_mr(project, iids: []) } - expect(result).to be_nil + expect(result).to be_empty end end - def resolve_mr(project, iid) - resolve(described_class, obj: project, args: { iid: iid }) + def resolve_mr(project, args) + resolve(described_class, obj: project, args: args) end end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 01d71abfac9..e8f1c84f8d6 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -6,12 +6,18 @@ describe GitlabSchema.types['Project'] do it { expect(described_class.graphql_name).to eq('Project') } describe 'nested merge request' do + it { expect(described_class).to have_graphql_field(:merge_requests) } it { expect(described_class).to have_graphql_field(:merge_request) } it 'authorizes the merge request' do expect(described_class.fields['mergeRequest']) .to require_graphql_authorizations(:read_merge_request) end + + it 'authorizes the merge requests' do + expect(described_class.fields['mergeRequests']) + .to require_graphql_authorizations(:read_merge_request) + end end describe 'nested issues' do diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index af4931e3370..6e8c13db9fe 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -39,59 +39,12 @@ describe ImportHelper do end end - describe '#provider_project_link' do - context 'when provider is "github"' do - let(:github_server_url) { nil } - let(:provider) { OpenStruct.new(name: 'github', url: github_server_url) } + describe '#provider_project_link_url' do + let(:full_path) { '/repo/path' } + let(:host_url) { 'http://provider.com/' } - before do - stub_omniauth_setting(providers: [provider]) - end - - context 'when provider does not specify a custom URL' do - it 'uses default GitHub URL' do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.com/octocat/Hello-World"') - end - end - - context 'when provider specify a custom URL' do - let(:github_server_url) { 'https://github.company.com' } - - it 'uses custom URL' do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.company.com/octocat/Hello-World"') - end - end - - context "when custom URL contains a '/' char at the end" do - let(:github_server_url) { 'https://github.company.com/' } - - it "doesn't render double slash" do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.company.com/octocat/Hello-World"') - end - end - - context 'when provider is missing' do - it 'uses the default URL' do - allow(Gitlab.config.omniauth).to receive(:providers).and_return([]) - - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.com/octocat/Hello-World"') - end - end - end - - context 'when provider is "gitea"' do - before do - assign(:gitea_host_url, 'https://try.gitea.io/') - end - - it 'uses given host' do - expect(helper.provider_project_link('gitea', 'octocat/Hello-World')) - .to include('href="https://try.gitea.io/octocat/Hello-World"') - end + it 'appends repo full path to provider host url' do + expect(helper.provider_project_link_url(host_url, full_path)).to match('http://provider.com/repo/path') end end end diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index a2cbc0f3c72..5abdfe695d0 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -68,6 +68,32 @@ describe('diffs/components/app', () => { }); }); + describe('resizable', () => { + afterEach(() => { + localStorage.removeItem('mr_tree_list_width'); + }); + + it('sets initial width when no localStorage has been set', () => { + createComponent(); + + expect(vm.vm.treeWidth).toEqual(320); + }); + + it('sets initial width to localStorage size', () => { + localStorage.setItem('mr_tree_list_width', '200'); + + createComponent(); + + expect(vm.vm.treeWidth).toEqual(200); + }); + + it('sets width of tree list', () => { + createComponent(); + + expect(vm.find('.js-diff-tree-list').element.style.width).toEqual('320px'); + }); + }); + describe('empty state', () => { it('renders empty state when no diff files exist', () => { createComponent(); diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js index 9e158327a77..a1bb51963d6 100644 --- a/spec/javascripts/diffs/components/diff_content_spec.js +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -6,6 +6,7 @@ import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; import '~/behaviors/markdown/render_gfm'; import diffFileMockData from '../mock_data/diff_file'; import discussionsMockData from '../mock_data/diff_discussions'; +import { diffViewerModes } from '~/ide/constants'; describe('DiffContent', () => { const Component = Vue.extend(DiffContentComponent); @@ -52,26 +53,39 @@ describe('DiffContent', () => { describe('empty files', () => { beforeEach(() => { - vm.diffFile.empty = true; vm.diffFile.highlighted_diff_lines = []; vm.diffFile.parallel_diff_lines = []; }); - it('should render a message', done => { + it('should render a no preview message if viewer returns no preview', done => { + vm.diffFile.viewer.name = diffViewerModes.no_preview; vm.$nextTick(() => { const block = vm.$el.querySelector('.diff-viewer .nothing-here-block'); expect(block).not.toBe(null); - expect(block.textContent.trim()).toContain('Empty file'); + expect(block.textContent.trim()).toContain('No preview for this file type'); + + done(); + }); + }); + + it('should render a not diffable message if viewer returns not diffable', done => { + vm.diffFile.viewer.name = diffViewerModes.not_diffable; + vm.$nextTick(() => { + const block = vm.$el.querySelector('.diff-viewer .nothing-here-block'); + + expect(block).not.toBe(null); + expect(block.textContent.trim()).toContain( + 'This diff was suppressed by a .gitattributes entry', + ); done(); }); }); it('should not render multiple messages', done => { - vm.diffFile.mode_changed = true; vm.diffFile.b_mode = '100755'; - vm.diffFile.viewer.name = 'mode_changed'; + vm.diffFile.viewer.name = diffViewerModes.mode_changed; vm.$nextTick(() => { expect(vm.$el.querySelectorAll('.nothing-here-block').length).toBe(1); @@ -81,6 +95,7 @@ describe('DiffContent', () => { }); it('should not render diff table', done => { + vm.diffFile.viewer.name = diffViewerModes.no_preview; vm.$nextTick(() => { expect(vm.$el.querySelector('table')).toBe(null); @@ -157,6 +172,7 @@ describe('DiffContent', () => { vm.diffFile.new_sha = 'DEF'; vm.diffFile.old_path = 'test.abc'; vm.diffFile.old_sha = 'ABC'; + vm.diffFile.viewer.name = diffViewerModes.added; vm.$nextTick(() => { expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 787a81fd88f..005a4751ea1 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -4,15 +4,15 @@ import diffsModule from '~/diffs/store/modules'; import notesModule from '~/notes/stores/modules'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffDiscussionsMockData from '../mock_data/diff_discussions'; +import { diffViewerModes } from '~/ide/constants'; Vue.use(Vuex); -const discussionFixture = 'merge_requests/diff_discussion.json'; - describe('diff_file_header', () => { let vm; let props; - const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; + const diffDiscussionMock = diffDiscussionsMockData; const Component = Vue.extend(DiffFileHeader); const store = new Vuex.Store({ @@ -303,13 +303,13 @@ describe('diff_file_header', () => { }); it('displays old and new path if the file was renamed', () => { - props.diffFile.renamed_file = true; + props.diffFile.viewer.name = diffViewerModes.renamed; vm = mountComponentWithStore(Component, { props, store }); expect(filePaths()).toHaveLength(2); - expect(filePaths()[0]).toHaveText(props.diffFile.old_path); - expect(filePaths()[1]).toHaveText(props.diffFile.new_path); + expect(filePaths()[0]).toHaveText(props.diffFile.old_path_html); + expect(filePaths()[1]).toHaveText(props.diffFile.new_path_html); }); }); @@ -319,14 +319,12 @@ describe('diff_file_header', () => { const button = vm.$el.querySelector('.btn-clipboard'); expect(button).not.toBe(null); - expect(button.dataset.clipboardText).toBe( - '{"text":"files/ruby/popen.rb","gfm":"`files/ruby/popen.rb`"}', - ); + expect(button.dataset.clipboardText).toBe('{"text":"CHANGELOG.rb","gfm":"`CHANGELOG.rb`"}'); }); describe('file mode', () => { it('it displays old and new file mode if it changed', () => { - props.diffFile.mode_changed = true; + props.diffFile.viewer.name = diffViewerModes.mode_changed; vm = mountComponentWithStore(Component, { props, store }); @@ -338,7 +336,7 @@ describe('diff_file_header', () => { }); it('does not display the file mode if it has not changed', () => { - props.diffFile.mode_changed = false; + props.diffFile.viewer.name = diffViewerModes.text; vm = mountComponentWithStore(Component, { props, store }); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 1af49282c36..65a1c9b8f15 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import DiffFileComponent from '~/diffs/components/diff_file.vue'; +import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; import store from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; @@ -27,7 +28,6 @@ describe('DiffFile', () => { expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); - expect(vm.file.renderIt).toEqual(false); vm.file.renderIt = true; vm.$nextTick(() => { @@ -38,8 +38,8 @@ describe('DiffFile', () => { describe('collapsed', () => { it('should not have file content', done => { expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1); - expect(vm.file.collapsed).toEqual(false); - vm.file.collapsed = true; + expect(vm.isCollapsed).toEqual(false); + vm.isCollapsed = true; vm.file.renderIt = true; vm.$nextTick(() => { @@ -50,9 +50,8 @@ describe('DiffFile', () => { }); it('should have collapsed text and link', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; - vm.file.highlighted_diff_lines = null; + vm.renderIt = true; + vm.isCollapsed = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain('This diff is collapsed'); @@ -63,8 +62,8 @@ describe('DiffFile', () => { }); it('should have collapsed text and link even before rendered', done => { - vm.file.renderIt = false; - vm.file.collapsed = true; + vm.renderIt = false; + vm.isCollapsed = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain('This diff is collapsed'); @@ -75,10 +74,10 @@ describe('DiffFile', () => { }); it('should be collapsed for renamed files', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; + vm.renderIt = true; + vm.isCollapsed = false; vm.file.highlighted_diff_lines = null; - vm.file.renamed_file = true; + vm.file.viewer.name = diffViewerModes.renamed; vm.$nextTick(() => { expect(vm.$el.innerText).not.toContain('This diff is collapsed'); @@ -88,10 +87,10 @@ describe('DiffFile', () => { }); it('should be collapsed for mode changed files', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; + vm.renderIt = true; + vm.isCollapsed = false; vm.file.highlighted_diff_lines = null; - vm.file.mode_changed = true; + vm.file.viewer.name = diffViewerModes.mode_changed; vm.$nextTick(() => { expect(vm.$el.innerText).not.toContain('This diff is collapsed'); @@ -101,7 +100,7 @@ describe('DiffFile', () => { }); it('should have loading icon while loading a collapsed diffs', done => { - vm.file.collapsed = true; + vm.isCollapsed = true; vm.isLoadingCollapsedDiff = true; vm.$nextTick(() => { @@ -116,7 +115,7 @@ describe('DiffFile', () => { describe('too large diff', () => { it('should have too large warning and blob link', done => { const BLOB_LINK = '/file/view/path'; - vm.file.too_large = true; + vm.file.viewer.error = diffViewerErrors.too_large; vm.file.view_path = BLOB_LINK; vm.$nextTick(() => { @@ -140,11 +139,11 @@ describe('DiffFile', () => { vm.file.highlighted_diff_lines = undefined; vm.file.parallel_diff_lines = []; - vm.file.collapsed = true; + vm.isCollapsed = true; vm.$nextTick() .then(() => { - vm.file.collapsed = false; + vm.isCollapsed = false; return vm.$nextTick(); }) diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 9e556698f34..cd7bf6405e5 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -28,7 +28,7 @@ describe('Diffs tree list component', () => { localStorage.removeItem('mr_diff_tree_list'); - vm = mountComponentWithStore(Component, { store }); + vm = mountComponentWithStore(Component, { store, props: { hideFileStats: false } }); }); afterEach(() => { @@ -77,6 +77,16 @@ describe('Diffs tree list component', () => { expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app'); }); + it('hides file stats', done => { + vm.hideFileStats = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-row-stats')).toBe(null); + + done(); + }); + }); + it('calls toggleTreeOpen when clicking folder', () => { spyOn(vm.$store, 'dispatch').and.stub(); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index c1e9f791925..4a091b4580b 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -266,7 +266,7 @@ export default { blob_name: 'CHANGELOG', blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', - file_path: 'CHANGELOG', + file_path: 'CHANGELOG.rb', new_file: false, deleted_file: false, renamed_file: false, @@ -286,7 +286,7 @@ export default { content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', stored_externally: null, external_storage: null, - old_path_html: ['CHANGELOG', 'CHANGELOG'], + old_path_html: 'CHANGELOG_OLD', new_path_html: 'CHANGELOG', context_lines_path: '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', @@ -485,6 +485,10 @@ export default { }, }, ], + viewer: { + name: 'text', + error: null, + }, }, diff_discussion: true, truncated_diff_lines: [ diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index 031c9842f2f..32af9ea8ddd 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -25,6 +25,8 @@ export default { text: true, viewer: { name: 'text', + error: null, + collapsed: false, }, added_lines: 2, removed_lines: 0, diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index b53ae4cecfd..acff80bca62 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -29,6 +29,7 @@ import actions, { renderFileForDiscussionId, setRenderTreeList, setShowWhitespace, + setRenderIt, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -262,12 +263,16 @@ describe('DiffsStoreActions', () => { { id: 1, renderIt: false, - collapsed: false, + viewer: { + collapsed: false, + }, }, { id: 2, renderIt: false, - collapsed: false, + viewer: { + collapsed: false, + }, }, ], }; @@ -766,7 +771,9 @@ describe('DiffsStoreActions', () => { diffFiles: [ { file_hash: 'HASH', - collapsed, + viewer: { + collapsed, + }, renderIt, }, ], @@ -849,4 +856,10 @@ describe('DiffsStoreActions', () => { expect(window.history.pushState).toHaveBeenCalled(); }); }); + + describe('setRenderIt', () => { + it('commits RENDER_FILE', done => { + testAction(setRenderIt, 'file', {}, [{ type: types.RENDER_FILE, payload: 'file' }], [], done); + }); + }); }); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 4f69dc92ab8..0ab88e6b2aa 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -51,13 +51,13 @@ describe('Diffs Module Getters', () => { describe('hasCollapsedFile', () => { it('returns true when all files are collapsed', () => { - localState.diffFiles = [{ collapsed: true }, { collapsed: true }]; + localState.diffFiles = [{ viewer: { collapsed: true } }, { viewer: { collapsed: true } }]; expect(getters.hasCollapsedFile(localState)).toEqual(true); }); it('returns true when at least one file is collapsed', () => { - localState.diffFiles = [{ collapsed: false }, { collapsed: true }]; + localState.diffFiles = [{ viewer: { collapsed: false } }, { viewer: { collapsed: true } }]; expect(getters.hasCollapsedFile(localState)).toEqual(true); }); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index a6f3f9b9dc3..09ee691b602 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -121,8 +121,14 @@ describe('DiffsStoreMutations', () => { describe('ADD_COLLAPSED_DIFFS', () => { it('should update the state with the given data for the given file hash', () => { const fileHash = 123; - const state = { diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }] }; - const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existing_field: 1 }] }; + const state = { + diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }], + }; + const data = { + diff_files: [ + { file_hash: fileHash, extra_field: 1, existing_field: 1, viewer: { name: 'text' } }, + ], + }; mutations[types.ADD_COLLAPSED_DIFFS](state, { file: state.diffFiles[1], data }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index c5e413a29d8..599ea9cd420 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -14,7 +14,7 @@ import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants'; import diffFileMockData from '../mock_data/diff_file'; import { noteableDataMock } from '../../notes/mock_data'; -const getDiffFileMock = () => Object.assign({}, diffFileMockData); +const getDiffFileMock = () => JSON.parse(JSON.stringify(diffFileMockData)); describe('DiffsStoreUtils', () => { describe('findDiffFile', () => { @@ -80,30 +80,44 @@ describe('DiffsStoreUtils', () => { }); describe('addContextLines', () => { - it('should add context lines properly with bottom parameter', () => { + it('should add context lines', () => { const diffFile = getDiffFileMock(); const inlineLines = diffFile.highlighted_diff_lines; const parallelLines = diffFile.parallel_diff_lines; const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; - const contextLines = [{ lineNumber: 42 }]; - const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true }; + const contextLines = [{ lineNumber: 42, line_code: '123' }]; + const options = { inlineLines, parallelLines, contextLines, lineNumbers }; const inlineIndex = utils.findIndexInInlineLines(inlineLines, lineNumbers); const parallelIndex = utils.findIndexInParallelLines(parallelLines, lineNumbers); const normalizedParallelLine = { left: options.contextLines[0], right: options.contextLines[0], + line_code: '123', }; utils.addContextLines(options); - expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); - expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine); + expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); + expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine); + }); + + it('should add context lines properly with bottom parameter', () => { + const diffFile = getDiffFileMock(); + const inlineLines = diffFile.highlighted_diff_lines; + const parallelLines = diffFile.parallel_diff_lines; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const contextLines = [{ lineNumber: 42, line_code: '123' }]; + const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true }; + const normalizedParallelLine = { + left: options.contextLines[0], + right: options.contextLines[0], + line_code: '123', + }; - delete options.bottom; utils.addContextLines(options); - expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); - expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine); + expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); + expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine); }); }); @@ -587,7 +601,7 @@ describe('DiffsStoreUtils', () => { it('returns mode_changed if key has no match', () => { expect( utils.getDiffMode({ - mode_changed: true, + viewer: { name: 'mode_changed' }, }), ).toBe('mode_changed'); }); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 9aa3cbaa231..6230da77f49 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -755,6 +755,17 @@ describe('Filtered Search Visual Tokens', () => { expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); }); + it('does not update user token appearance for `None` filter', () => { + const { tokenNameElement } = findElements(authorToken); + + const tokenName = tokenNameElement.innerText; + const tokenValue = 'None'; + + subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + it('does not update user token appearance for `none` filter', () => { const { tokenNameElement } = findElements(authorToken); diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index 1972408356e..88652202a8e 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -84,7 +84,10 @@ export default ( done(); }; - const result = action({ commit, state, dispatch, rootState: state, rootGetters: state }, payload); + const result = action( + { commit, state, dispatch, rootState: state, rootGetters: state, getters: state }, + payload, + ); return new Promise(resolve => { setImmediate(resolve); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index 595a2f927e9..d94cc1a8faa 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -41,6 +41,15 @@ describe('new file modal component', () => { expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name'); }); + it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => { + const templateFilesEl = vm.$el.querySelector('.file-templates'); + if (type === 'tree') { + expect(templateFilesEl).toBeNull(); + } else { + expect(templateFilesEl instanceof Element).toBeTruthy(); + } + }); + describe('createEntryInStore', () => { it('$emits create', () => { spyOn(vm, 'createTempEntry'); diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js new file mode 100644 index 00000000000..a1ff84ce259 --- /dev/null +++ b/spec/javascripts/import_projects/components/import_projects_table_spec.js @@ -0,0 +1,186 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import store from '~/import_projects/store'; +import importProjectsTable from '~/import_projects/components/import_projects_table.vue'; +import STATUS_MAP from '~/import_projects/constants'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +describe('ImportProjectsTable', () => { + let vm; + let mock; + const reposPath = '/repos-path'; + const jobsPath = '/jobs-path'; + const providerTitle = 'THE PROVIDER'; + const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' }; + const importedProject = { + id: 1, + fullPath: 'fullPath', + importStatus: 'started', + providerLink: 'providerLink', + importSource: 'importSource', + }; + + function createComponent() { + const ImportProjectsTable = Vue.extend(importProjectsTable); + + const component = new ImportProjectsTable({ + store, + propsData: { + providerTitle, + }, + }).$mount(); + + component.$store.dispatch('stopJobsPolling'); + + return component; + } + + beforeEach(() => { + store.dispatch('setInitialData', { reposPath }); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + vm.$destroy(); + mock.restore(); + }); + + it('renders a loading icon whilst repos are loading', done => { + mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('renders a table with imported projects and provider repos', done => { + const response = { + importedProjects: [importedProject], + providerRepos: [providerRepo], + namespaces: [{ path: 'path' }], + }; + mock.onGet(reposPath).reply(200, response); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull(); + expect(vm.$el.querySelector('.table')).not.toBeNull(); + expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch( + `From ${providerTitle}`, + ); + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('renders an empty state if there are no imported projects or provider repos', done => { + const response = { + importedProjects: [], + providerRepos: [], + namespaces: [], + }; + mock.onGet(reposPath).reply(200, response); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull(); + expect(vm.$el.querySelector('.table')).toBeNull(); + expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('imports provider repos if bulk import button is clicked', done => { + const importPath = '/import-path'; + const response = { + importedProjects: [], + providerRepos: [providerRepo], + namespaces: [{ path: 'path' }], + }; + + mock.onGet(reposPath).replyOnce(200, response); + mock.onPost(importPath).replyOnce(200, importedProject); + + store.dispatch('setInitialData', { importPath }); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-imported-project')).toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull(); + + vm.$el.querySelector('.js-import-all').click(); + }) + .then(() => setTimeoutPromise()) + .then(() => { + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('polls to update the status of imported projects', done => { + const importPath = '/import-path'; + const response = { + importedProjects: [importedProject], + providerRepos: [], + namespaces: [{ path: 'path' }], + }; + const updatedProjects = [ + { + id: importedProject.id, + importStatus: 'finished', + }, + ]; + + mock.onGet(reposPath).replyOnce(200, response); + + store.dispatch('setInitialData', { importPath, jobsPath }); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + const statusObject = STATUS_MAP[importedProject.importStatus]; + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + + mock.onGet(jobsPath).replyOnce(200, updatedProjects); + return vm.$store.dispatch('restartJobsPolling'); + }) + .then(() => setTimeoutPromise()) + .then(() => { + const statusObject = STATUS_MAP[updatedProjects[0].importStatus]; + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); +}); diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js new file mode 100644 index 00000000000..8af3b5954a9 --- /dev/null +++ b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import store from '~/import_projects/store'; +import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue'; +import STATUS_MAP from '~/import_projects/constants'; + +describe('ImportedProjectTableRow', () => { + let vm; + const project = { + id: 1, + fullPath: 'fullPath', + importStatus: 'finished', + providerLink: 'providerLink', + importSource: 'importSource', + }; + + function createComponent() { + const ImportedProjectTableRow = Vue.extend(importedProjectTableRow); + + return new ImportedProjectTableRow({ + store, + propsData: { + project: { + ...project, + }, + }, + }).$mount(); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders an imported project table row', () => { + vm = createComponent(); + + const providerLink = vm.$el.querySelector('.js-provider-link'); + const statusObject = STATUS_MAP[project.importStatus]; + + expect(vm.$el.classList.contains('js-imported-project')).toBe(true); + expect(providerLink.href).toMatch(project.providerLink); + expect(providerLink.textContent).toMatch(project.importSource); + expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath); + }); +}); diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js new file mode 100644 index 00000000000..7191fc923ce --- /dev/null +++ b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js @@ -0,0 +1,78 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import store from '~/import_projects/store'; +import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue'; +import STATUS_MAP, { STATUSES } from '~/import_projects/constants'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +describe('ProviderRepoTableRow', () => { + let vm; + const repo = { + id: 10, + sanitizedName: 'sanitizedName', + fullName: 'fullName', + providerLink: 'providerLink', + }; + + function createComponent() { + const ProviderRepoTableRow = Vue.extend(providerRepoTableRow); + + return new ProviderRepoTableRow({ + store, + propsData: { + repo: { + ...repo, + }, + }, + }).$mount(); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a provider repo table row', () => { + vm = createComponent(); + + const providerLink = vm.$el.querySelector('.js-provider-link'); + const statusObject = STATUS_MAP[STATUSES.NONE]; + + expect(vm.$el.classList.contains('js-provider-repo')).toBe(true); + expect(providerLink.href).toMatch(repo.providerLink); + expect(providerLink.textContent).toMatch(repo.fullName); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + expect(vm.$el.querySelector('.js-import-button')).not.toBeNull(); + }); + + it('imports repo when clicking import button', done => { + const importPath = '/import-path'; + const defaultTargetNamespace = 'user'; + const ciCdOnly = true; + const mock = new MockAdapter(axios); + + store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly }); + mock.onPost(importPath).replyOnce(200); + spyOn(store, 'dispatch').and.returnValue(new Promise(() => {})); + + vm = createComponent(); + + vm.$el.querySelector('.js-import-button').click(); + + setTimeoutPromise() + .then(() => { + expect(store.dispatch).toHaveBeenCalledWith('fetchImport', { + repo, + newName: repo.sanitizedName, + targetNamespace: defaultTargetNamespace, + }); + }) + .then(() => mock.restore()) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/javascripts/import_projects/store/actions_spec.js new file mode 100644 index 00000000000..77850ee3283 --- /dev/null +++ b/spec/javascripts/import_projects/store/actions_spec.js @@ -0,0 +1,284 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { + SET_INITIAL_DATA, + REQUEST_REPOS, + RECEIVE_REPOS_SUCCESS, + RECEIVE_REPOS_ERROR, + REQUEST_IMPORT, + RECEIVE_IMPORT_SUCCESS, + RECEIVE_IMPORT_ERROR, + RECEIVE_JOBS_SUCCESS, +} from '~/import_projects/store/mutation_types'; +import { + setInitialData, + requestRepos, + receiveReposSuccess, + receiveReposError, + fetchRepos, + requestImport, + receiveImportSuccess, + receiveImportError, + fetchImport, + receiveJobsSuccess, + fetchJobs, + clearJobsEtagPoll, + stopJobsPolling, +} from '~/import_projects/store/actions'; +import state from '~/import_projects/store/state'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('import_projects store actions', () => { + let localState; + const repoId = 1; + const repos = [{ id: 1 }, { id: 2 }]; + const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } }; + + beforeEach(() => { + localState = state(); + }); + + describe('setInitialData', () => { + it(`commits ${SET_INITIAL_DATA} mutation`, done => { + const initialData = { + reposPath: 'reposPath', + provider: 'provider', + jobsPath: 'jobsPath', + importPath: 'impapp/assets/javascripts/vue_shared/components/select2_select.vueortPath', + defaultTargetNamespace: 'defaultTargetNamespace', + ciCdOnly: 'ciCdOnly', + canSelectNamespace: 'canSelectNamespace', + }; + + testAction( + setInitialData, + initialData, + localState, + [{ type: SET_INITIAL_DATA, payload: initialData }], + [], + done, + ); + }); + }); + + describe('requestRepos', () => { + it(`requestRepos commits ${REQUEST_REPOS} mutation`, done => { + testAction( + requestRepos, + null, + localState, + [{ type: REQUEST_REPOS, payload: null }], + [], + done, + ); + }); + }); + + describe('receiveReposSuccess', () => { + it(`commits ${RECEIVE_REPOS_SUCCESS} mutation`, done => { + testAction( + receiveReposSuccess, + repos, + localState, + [{ type: RECEIVE_REPOS_SUCCESS, payload: repos }], + [], + done, + ); + }); + }); + + describe('receiveReposError', () => { + it(`commits ${RECEIVE_REPOS_ERROR} mutation`, done => { + testAction(receiveReposError, repos, localState, [{ type: RECEIVE_REPOS_ERROR }], [], done); + }); + }); + + describe('fetchRepos', () => { + let mock; + + beforeEach(() => { + localState.reposPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => { + const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] }; + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload); + + testAction( + fetchRepos, + null, + localState, + [], + [ + { type: 'requestRepos' }, + { + type: 'receiveReposSuccess', + payload: convertObjectPropsToCamelCase(payload, { deep: true }), + }, + { + type: 'fetchJobs', + }, + ], + done, + ); + }); + + it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + + testAction( + fetchRepos, + null, + localState, + [], + [{ type: 'requestRepos' }, { type: 'receiveReposError' }], + done, + ); + }); + }); + + describe('requestImport', () => { + it(`commits ${REQUEST_IMPORT} mutation`, done => { + testAction( + requestImport, + repoId, + localState, + [{ type: REQUEST_IMPORT, payload: repoId }], + [], + done, + ); + }); + }); + + describe('receiveImportSuccess', () => { + it(`commits ${RECEIVE_IMPORT_SUCCESS} mutation`, done => { + const payload = { importedProject: { name: 'imported/project' }, repoId: 2 }; + + testAction( + receiveImportSuccess, + payload, + localState, + [{ type: RECEIVE_IMPORT_SUCCESS, payload }], + [], + done, + ); + }); + }); + + describe('receiveImportError', () => { + it(`commits ${RECEIVE_IMPORT_ERROR} mutation`, done => { + testAction( + receiveImportError, + repoId, + localState, + [{ type: RECEIVE_IMPORT_ERROR, payload: repoId }], + [], + done, + ); + }); + }); + + describe('fetchImport', () => { + let mock; + + beforeEach(() => { + localState.importPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestImport and receiveImportSuccess actions on a successful request', done => { + const importedProject = { name: 'imported/project' }; + const importRepoId = importPayload.repo.id; + mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject); + + testAction( + fetchImport, + importPayload, + localState, + [], + [ + { type: 'requestImport', payload: importRepoId }, + { + type: 'receiveImportSuccess', + payload: { + importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }), + repoId: importRepoId, + }, + }, + ], + done, + ); + }); + + it('dispatches requestImport and receiveImportSuccess actions on an unsuccessful request', done => { + mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500); + + testAction( + fetchImport, + importPayload, + localState, + [], + [ + { type: 'requestImport', payload: importPayload.repo.id }, + { type: 'receiveImportError', payload: { repoId: importPayload.repo.id } }, + ], + done, + ); + }); + }); + + describe('receiveJobsSuccess', () => { + it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, done => { + testAction( + receiveJobsSuccess, + repos, + localState, + [{ type: RECEIVE_JOBS_SUCCESS, payload: repos }], + [], + done, + ); + }); + }); + + describe('fetchJobs', () => { + let mock; + + beforeEach(() => { + localState.jobsPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + stopJobsPolling(); + clearJobsEtagPoll(); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => { + const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }]; + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects); + + testAction( + fetchJobs, + null, + localState, + [], + [ + { + type: 'receiveJobsSuccess', + payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }), + }, + ], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/import_projects/store/getters_spec.js b/spec/javascripts/import_projects/store/getters_spec.js new file mode 100644 index 00000000000..e5e4a95f473 --- /dev/null +++ b/spec/javascripts/import_projects/store/getters_spec.js @@ -0,0 +1,83 @@ +import { + namespaceSelectOptions, + isImportingAnyRepo, + hasProviderRepos, + hasImportedProjects, +} from '~/import_projects/store/getters'; +import state from '~/import_projects/store/state'; + +describe('import_projects store getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('namespaceSelectOptions', () => { + const namespaces = [{ fullPath: 'namespace-0' }, { fullPath: 'namespace-1' }]; + const defaultTargetNamespace = 'current-user'; + + it('returns an options array with a "Users" and "Groups" optgroups', () => { + localState.namespaces = namespaces; + localState.defaultTargetNamespace = defaultTargetNamespace; + + const optionsArray = namespaceSelectOptions(localState); + const groupsGroup = optionsArray[0]; + const usersGroup = optionsArray[1]; + + expect(groupsGroup.text).toBe('Groups'); + expect(usersGroup.text).toBe('Users'); + + groupsGroup.children.forEach((child, index) => { + expect(child.id).toBe(namespaces[index].fullPath); + expect(child.text).toBe(namespaces[index].fullPath); + }); + + expect(usersGroup.children.length).toBe(1); + expect(usersGroup.children[0].id).toBe(defaultTargetNamespace); + expect(usersGroup.children[0].text).toBe(defaultTargetNamespace); + }); + }); + + describe('isImportingAnyRepo', () => { + it('returns true if there are any reposBeingImported', () => { + localState.reposBeingImported = new Array(1); + + expect(isImportingAnyRepo(localState)).toBe(true); + }); + + it('returns false if there are no reposBeingImported', () => { + localState.reposBeingImported = []; + + expect(isImportingAnyRepo(localState)).toBe(false); + }); + }); + + describe('hasProviderRepos', () => { + it('returns true if there are any providerRepos', () => { + localState.providerRepos = new Array(1); + + expect(hasProviderRepos(localState)).toBe(true); + }); + + it('returns false if there are no providerRepos', () => { + localState.providerRepos = []; + + expect(hasProviderRepos(localState)).toBe(false); + }); + }); + + describe('hasImportedProjects', () => { + it('returns true if there are any importedProjects', () => { + localState.importedProjects = new Array(1); + + expect(hasImportedProjects(localState)).toBe(true); + }); + + it('returns false if there are no importedProjects', () => { + localState.importedProjects = []; + + expect(hasImportedProjects(localState)).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/import_projects/store/mutations_spec.js b/spec/javascripts/import_projects/store/mutations_spec.js new file mode 100644 index 00000000000..8db8e9819ba --- /dev/null +++ b/spec/javascripts/import_projects/store/mutations_spec.js @@ -0,0 +1,34 @@ +import * as types from '~/import_projects/store/mutation_types'; +import mutations from '~/import_projects/store/mutations'; + +describe('import_projects store mutations', () => { + describe(types.RECEIVE_IMPORT_SUCCESS, () => { + it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => { + const repoId = 1; + const state = { + reposBeingImported: [repoId], + providerRepos: [{ id: repoId }], + importedProjects: [], + }; + const importedProject = { id: repoId }; + + mutations[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }); + + expect(state.reposBeingImported.includes(repoId)).toBe(false); + expect(state.providerRepos.some(repo => repo.id === repoId)).toBe(false); + expect(state.importedProjects.some(repo => repo.id === repoId)).toBe(true); + }); + }); + + describe(types.RECEIVE_JOBS_SUCCESS, () => { + it('updates importStatus of existing importedProjects', () => { + const repoId = 1; + const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] }; + const updatedProjects = [{ id: repoId, importStatus: 'finished' }]; + + mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects); + + expect(state.importedProjects[0].importStatus).toBe(updatedProjects[0].importStatus); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 3eff3f655ee..c02e37950f8 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -855,6 +855,7 @@ describe('common_utils', () => { }); it('returns true when provided `el` is in viewport', () => { + el.setAttribute('style', `position: absolute; right: ${window.innerWidth + 0.2};`); document.body.appendChild(el); expect(commonUtils.isInViewport(el)).toBe(true); diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/javascripts/notes/components/note_actions/reply_button_spec.js index 11e1664a3f4..11fb89808d9 100644 --- a/spec/javascripts/notes/components/note_actions/reply_button_spec.js +++ b/spec/javascripts/notes/components/note_actions/reply_button_spec.js @@ -3,27 +3,14 @@ import { createLocalVue, mount } from '@vue/test-utils'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; describe('ReplyButton', () => { - const noteId = 'dummy-note-id'; - let wrapper; - let convertToDiscussion; beforeEach(() => { const localVue = createLocalVue(); - convertToDiscussion = jasmine.createSpy('convertToDiscussion'); localVue.use(Vuex); - const store = new Vuex.Store({ - actions: { - convertToDiscussion, - }, - }); wrapper = mount(ReplyButton, { - propsData: { - noteId, - }, - store, sync: false, localVue, }); @@ -33,14 +20,13 @@ describe('ReplyButton', () => { wrapper.destroy(); }); - it('dispatches convertToDiscussion with note ID on click', () => { + it('emits startReplying on click', () => { const button = wrapper.find({ ref: 'button' }); button.trigger('click'); - expect(convertToDiscussion).toHaveBeenCalledTimes(1); - const [, payload] = convertToDiscussion.calls.argsFor(0); - - expect(payload).toBe(noteId); + expect(wrapper.emitted()).toEqual({ + startReplying: [[]], + }); }); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 0c1962912b4..d604e90b529 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount, createLocalVue, createWrapper } from '@vue/test-utils'; import createStore from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; import { TEST_HOST } from 'spec/test_constants'; @@ -10,7 +10,7 @@ describe('noteActions', () => { let store; let props; - const createWrapper = propsData => { + const shallowMountNoteActions = propsData => { const localVue = createLocalVue(); return shallowMount(noteActions, { store, @@ -44,7 +44,7 @@ describe('noteActions', () => { beforeEach(() => { store.dispatch('setUserData', userDataMock); - wrapper = createWrapper(props); + wrapper = shallowMountNoteActions(props); }); it('should render access level badge', () => { @@ -90,13 +90,27 @@ describe('noteActions', () => { it('should be possible to delete comment', () => { expect(wrapper.find('.js-note-delete').exists()).toBe(true); }); + + it('closes tooltip when dropdown opens', done => { + wrapper.find('.more-actions-toggle').trigger('click'); + + const rootWrapper = createWrapper(wrapper.vm.$root); + Vue.nextTick() + .then(() => { + const emitted = Object.keys(rootWrapper.emitted()); + + expect(emitted).toEqual(['bv::hide::tooltip']); + done(); + }) + .catch(done.fail); + }); }); }); describe('user is not logged in', () => { beforeEach(() => { store.dispatch('setUserData', {}); - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, canDelete: false, canEdit: false, @@ -127,7 +141,7 @@ describe('noteActions', () => { describe('for showReply = true', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: true, }); @@ -142,7 +156,7 @@ describe('noteActions', () => { describe('for showReply = false', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: false, }); @@ -169,7 +183,7 @@ describe('noteActions', () => { describe('for showReply = true', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: true, }); @@ -184,7 +198,7 @@ describe('noteActions', () => { describe('for showReply = false', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: false, }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index 8ade6fc2ced..9420713ceca 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,86 +1,138 @@ -import $ from 'jquery'; import _ from 'underscore'; -import Vue from 'vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import createStore from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; +import NoteHeader from '~/notes/components/note_header.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import NoteActions from '~/notes/components/note_actions.vue'; +import NoteBody from '~/notes/components/note_body.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note', () => { let store; - let vm; + let wrapper; beforeEach(() => { - const Component = Vue.extend(issueNote); - store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); - vm = new Component({ + const localVue = createLocalVue(); + wrapper = shallowMount(issueNote, { store, propsData: { note, }, - }).$mount(); + sync: false, + localVue, + }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('should render user information', () => { - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual( - note.author.avatar_url, - ); + const { author } = note; + const avatar = wrapper.find(UserAvatarLink); + const avatarProps = avatar.props(); + + expect(avatarProps.linkHref).toBe(author.path); + expect(avatarProps.imgSrc).toBe(author.avatar_url); + expect(avatarProps.imgAlt).toBe(author.name); + expect(avatarProps.imgSize).toBe(40); }); it('should render note header content', () => { - const el = vm.$el.querySelector('.note-header .note-header-author-name'); + const noteHeader = wrapper.find(NoteHeader); + const noteHeaderProps = noteHeader.props(); - expect(el.textContent.trim()).toEqual(note.author.name); + expect(noteHeaderProps.author).toEqual(note.author); + expect(noteHeaderProps.createdAt).toEqual(note.created_at); + expect(noteHeaderProps.noteId).toEqual(note.id); }); it('should render note actions', () => { - expect(vm.$el.querySelector('.note-actions')).toBeDefined(); + const { author } = note; + const noteActions = wrapper.find(NoteActions); + const noteActionsProps = noteActions.props(); + + expect(noteActionsProps.authorId).toBe(author.id); + expect(noteActionsProps.noteId).toBe(note.id); + expect(noteActionsProps.noteUrl).toBe(note.noteable_note_url); + expect(noteActionsProps.accessLevel).toBe(note.human_access); + expect(noteActionsProps.canEdit).toBe(note.current_user.can_edit); + expect(noteActionsProps.canAwardEmoji).toBe(note.current_user.can_award_emoji); + expect(noteActionsProps.canDelete).toBe(note.current_user.can_edit); + expect(noteActionsProps.canReportAsAbuse).toBe(true); + expect(noteActionsProps.canResolve).toBe(false); + expect(noteActionsProps.reportAbusePath).toBe(note.report_abuse_path); + expect(noteActionsProps.resolvable).toBe(false); + expect(noteActionsProps.isResolved).toBe(false); + expect(noteActionsProps.isResolving).toBe(false); + expect(noteActionsProps.resolvedBy).toEqual({}); }); it('should render issue body', () => { - expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); + const noteBody = wrapper.find(NoteBody); + const noteBodyProps = noteBody.props(); + + expect(noteBodyProps.note).toEqual(note); + expect(noteBodyProps.line).toBe(null); + expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit); + expect(noteBodyProps.isEditing).toBe(false); + expect(noteBodyProps.helpPagePath).toBe(''); }); it('prevents note preview xss', done => { const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; const alertSpy = spyOn(window, 'alert'); - vm.updateNote = () => new Promise($.noop); + store.hotUpdate({ + actions: { + updateNote() {}, + }, + }); + const noteBodyComponent = wrapper.find(NoteBody); - vm.formUpdateHandler(noteBody, null, $.noop); + noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {}); setTimeout(() => { expect(alertSpy).not.toHaveBeenCalled(); - expect(vm.note.note_html).toEqual(_.escape(noteBody)); + expect(wrapper.vm.note.note_html).toEqual(_.escape(noteBody)); done(); }, 0); }); describe('cancel edit', () => { it('restores content of updated note', done => { - const noteBody = 'updated note text'; - vm.updateNote = () => Promise.resolve(); - - vm.formUpdateHandler(noteBody, null, $.noop); - - setTimeout(() => { - expect(vm.note.note_html).toEqual(noteBody); - - vm.formCancelHandler(); - - setTimeout(() => { - expect(vm.note.note_html).toEqual(noteBody); - - done(); - }); + const updatedText = 'updated note text'; + store.hotUpdate({ + actions: { + updateNote() {}, + }, }); + const noteBody = wrapper.find(NoteBody); + noteBody.vm.resetAutoSave = () => {}; + + noteBody.vm.$emit('handleFormUpdate', updatedText, null, () => {}); + + wrapper.vm + .$nextTick() + .then(() => { + const noteBodyProps = noteBody.props(); + + expect(noteBodyProps.note.note_html).toBe(updatedText); + noteBody.vm.$emit('cancelForm'); + }) + .then(() => wrapper.vm.$nextTick()) + .then(() => { + const noteBodyProps = noteBody.props(); + + expect(noteBodyProps.note.note_html).toBe(note.note_html); + }) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 73f960dd21e..94ce6d8e222 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,8 +1,11 @@ import Vue from 'vue'; import $ from 'jquery'; import _ from 'underscore'; +import { TEST_HOST } from 'spec/test_constants'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; +import * as mutationTypes from '~/notes/stores/mutation_types'; +import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; import testAction from '../../helpers/vuex_action_helper'; @@ -599,4 +602,153 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('updateOrCreateNotes', () => { + let commit; + let dispatch; + let state; + + beforeEach(() => { + commit = jasmine.createSpy('commit'); + dispatch = jasmine.createSpy('dispatch'); + state = {}; + }); + + afterEach(() => { + commit.calls.reset(); + dispatch.calls.reset(); + }); + + it('Updates existing note', () => { + const note = { id: 1234 }; + const getters = { notesById: { 1234: note } }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.UPDATE_NOTE, note]]); + }); + + it('Creates a new note if none exisits', () => { + const note = { id: 1234 }; + const getters = { notesById: {} }; + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, note]]); + }); + + describe('Discussion notes', () => { + let note; + let getters; + + beforeEach(() => { + note = { id: 1234 }; + getters = { notesById: {} }; + }); + + it('Adds a reply to an existing discussion', () => { + state = { discussions: [note] }; + const discussionNote = { + ...note, + type: notesConstants.DISCUSSION_NOTE, + discussion_id: 1234, + }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]); + + expect(commit.calls.allArgs()).toEqual([ + [mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, discussionNote], + ]); + }); + + it('fetches discussions for diff notes', () => { + state = { discussions: [], notesData: { discussionsPath: 'Hello world' } }; + const diffNote = { ...note, type: notesConstants.DIFF_NOTE, discussion_id: 1234 }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [diffNote]); + + expect(dispatch.calls.allArgs()).toEqual([ + ['fetchDiscussions', { path: state.notesData.discussionsPath }], + ]); + }); + + it('Adds a new note', () => { + state = { discussions: [] }; + const discussionNote = { + ...note, + type: notesConstants.DISCUSSION_NOTE, + discussion_id: 1234, + }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, discussionNote]]); + }); + }); + }); + + describe('replyToDiscussion', () => { + let res = { discussion: { notes: [] } }; + const payload = { endpoint: TEST_HOST, data: {} }; + const interceptor = (request, next) => { + next( + request.respondWith(JSON.stringify(res), { + status: 200, + }), + ); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('updates discussion if response contains disussion', done => { + testAction( + actions.replyToDiscussion, + payload, + { + notesById: {}, + }, + [{ type: mutationTypes.UPDATE_DISCUSSION, payload: res.discussion }], + [ + { type: 'updateMergeRequestWidget' }, + { type: 'startTaskList' }, + { type: 'updateResolvableDiscussonsCounts' }, + ], + done, + ); + }); + + it('adds a reply to a discussion', done => { + res = {}; + + testAction( + actions.replyToDiscussion, + payload, + { + notesById: {}, + }, + [{ type: mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, payload: res }], + [], + done, + ); + }); + }); + + describe('removeConvertedDiscussion', () => { + it('commits CONVERT_TO_DISCUSSION with noteId', done => { + const noteId = 'dummy-id'; + testAction( + actions.removeConvertedDiscussion, + noteId, + {}, + [{ type: 'REMOVE_CONVERTED_DISCUSSION', payload: noteId }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 4f8d3069bb5..fcad1f245b6 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -527,17 +527,32 @@ describe('Notes Store mutations', () => { id: 42, individual_note: true, }; - state = { discussions: [discussion] }; + state = { convertedDisscussionIds: [] }; }); - it('toggles individual_note', () => { + it('adds a disucssion to convertedDisscussionIds', () => { mutations.CONVERT_TO_DISCUSSION(state, discussion.id); - expect(discussion.individual_note).toBe(false); + expect(state.convertedDisscussionIds).toContain(discussion.id); }); + }); + + describe('REMOVE_CONVERTED_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { convertedDisscussionIds: [41, 42] }; + }); + + it('removes a disucssion from convertedDisscussionIds', () => { + mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id); - it('throws if discussion was not found', () => { - expect(() => mutations.CONVERT_TO_DISCUSSION(state, 99)).toThrow(); + expect(state.convertedDisscussionIds).not.toContain(discussion.id); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index ff08a46b922..3e8f73646c8 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -21,6 +21,7 @@ describe('mrWidgetOptions', () => { const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch'; beforeEach(() => { + gon.features = { approvalRules: false }; // Prevent component mounting delete mrWidgetOptions.el; @@ -31,6 +32,7 @@ describe('mrWidgetOptions', () => { }); afterEach(() => { + gon.features = null; vm.$destroy(); }); diff --git a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js index 5b1038840c7..634ba8403d5 100644 --- a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js @@ -5,27 +5,40 @@ import createComponent from 'spec/helpers/vue_mount_component_helper'; describe('Changed file icon', () => { let vm; - beforeEach(() => { + function factory(props = {}) { const component = Vue.extend(changedFileIcon); vm = createComponent(component, { + ...props, file: { tempFile: false, changed: true, }, }); - }); + } afterEach(() => { vm.$destroy(); }); + it('centers icon', () => { + factory({ + isCentered: true, + }); + + expect(vm.$el.classList).toContain('ml-auto'); + }); + describe('changedIcon', () => { it('equals file-modified when not a temp file and has changes', () => { + factory(); + expect(vm.changedIcon).toBe('file-modified'); }); it('equals file-addition when a temp file', () => { + factory(); + vm.file.tempFile = true; expect(vm.changedIcon).toBe('file-addition'); @@ -34,10 +47,14 @@ describe('Changed file icon', () => { describe('changedIconClass', () => { it('includes file-modified when not a temp file', () => { + factory(); + expect(vm.changedIconClass).toContain('file-modified'); }); it('includes file-addition when a temp file', () => { + factory(); + vm.file.tempFile = true; expect(vm.changedIconClass).toContain('file-addition'); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js index 6add6cdac4d..660eaddf01f 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -22,6 +22,7 @@ describe('DiffViewer', () => { createComponent({ diffMode: 'replaced', + diffViewerMode: 'image', newPath: GREEN_BOX_IMAGE_URL, newSha: 'ABC', oldPath: RED_BOX_IMAGE_URL, @@ -45,6 +46,7 @@ describe('DiffViewer', () => { it('renders fallback download diff display', done => { createComponent({ diffMode: 'replaced', + diffViewerMode: 'added', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', @@ -72,6 +74,7 @@ describe('DiffViewer', () => { it('renders renamed component', () => { createComponent({ diffMode: 'renamed', + diffViewerMode: 'renamed', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', @@ -84,6 +87,7 @@ describe('DiffViewer', () => { it('renders mode changed component', () => { createComponent({ diffMode: 'mode_changed', + diffViewerMode: 'image', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js index 49a580be06b..caabe3aa260 100644 --- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js +++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js @@ -44,7 +44,10 @@ describe('Panel Resizer component', () => { }); expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-left'); + expect(vm.$el.getAttribute('class')).toBe( + 'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0', + ); + expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;'); }); @@ -55,7 +58,9 @@ describe('Panel Resizer component', () => { }); expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-right'); + expect(vm.$el.getAttribute('class')).toBe( + 'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0', + ); }); it('drag the resizer', () => { diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index f2472fd377c..80aa75847ae 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -1,13 +1,14 @@ import _ from 'underscore'; import Vue from 'vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { TEST_HOST } from 'spec/test_constants'; describe('User Avatar Link Component', function() { beforeEach(function() { this.propsData = { - linkHref: 'myavatarurl.com', + linkHref: `${TEST_HOST}/myavatarurl.com`, imgSize: 99, - imgSrc: 'myavatarurl.com', + imgSrc: `${TEST_HOST}/myavatarurl.com`, imgAlt: 'mydisplayname', imgCssClasses: 'myextraavatarclass', tooltipText: 'tooltip text', @@ -37,11 +38,18 @@ describe('User Avatar Link Component', function() { }); it('should render <a> as a child element', function() { - expect(this.userAvatarLink.$el.tagName).toBe('A'); + const link = this.userAvatarLink.$el; + + expect(link.tagName).toBe('A'); + expect(link.href).toBe(this.propsData.linkHref); }); - it('should have <img> as a child element', function() { - expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); + it('renders imgSrc with imgSize as image', function() { + const { imgSrc, imgSize } = this.propsData; + const image = this.userAvatarLink.$el.querySelector('img'); + + expect(image).not.toBeNull(); + expect(image.src).toBe(`${imgSrc}?width=${imgSize}`); }); it('should return necessary props as defined', function() { diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb index 2e50e4e2351..c6dcb4e46fd 100644 --- a/spec/lib/banzai/filter/footnote_filter_spec.rb +++ b/spec/lib/banzai/filter/footnote_filter_spec.rb @@ -11,6 +11,7 @@ describe Banzai::Filter::FootnoteFilter do let(:footnote) do <<~EOF <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p> + <p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p> <ol> <li id="fn1"> <p>one <a href="#fnref1">↩</a></p> @@ -25,6 +26,7 @@ describe Banzai::Filter::FootnoteFilter do let(:filtered_footnote) do <<~EOF <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p> + <p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p> <section class="footnotes"><ol> <li id="fn1-#{identifier}"> <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p> diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb index 5fee37bb43e..dc3329061d1 100644 --- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb @@ -73,9 +73,9 @@ describe Gitlab::Ci::Build::Policy::Changes do expect(policy).not_to be_satisfied_by(pipeline, seed) end - context 'when pipelines does not run for a branch update' do + context 'when modified paths can not be evaluated' do before do - pipeline.before_sha = Gitlab::Git::BLANK_SHA + allow(pipeline).to receive(:modified_paths) { nil } end it 'is always satisfied' do @@ -115,5 +115,57 @@ describe Gitlab::Ci::Build::Policy::Changes do expect(policy).not_to be_satisfied_by(pipeline, seed) end end + + context 'when branch is created' do + let(:pipeline) do + create(:ci_empty_pipeline, project: project, + ref: 'feature', + source: source, + sha: '0b4bc9a4', + before_sha: Gitlab::Git::BLANK_SHA, + merge_request: merge_request) + end + + let(:ci_build) do + build(:ci_build, pipeline: pipeline, project: project, ref: 'feature') + end + + let(:seed) { double('build seed', to_resource: ci_build) } + + context 'when source is merge request' do + let(:source) { :merge_request } + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'is satified by changes in the merge request' do + policy = described_class.new(%w[files/ruby/feature.rb]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is not satified by changes not in the merge request' do + policy = described_class.new(%w[foo.rb]) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + end + + context 'when source is push' do + let(:source) { :push } + let(:merge_request) { nil } + + it 'is always satified' do + policy = described_class.new(%w[foo.rb]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + end + end end end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb new file mode 100644 index 00000000000..793cac593a2 --- /dev/null +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -0,0 +1,300 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' +require 'webmock/rspec' + +require 'gitlab/danger/helper' + +describe Gitlab::Danger::Helper do + using RSpec::Parameterized::TableSyntax + + class FakeDanger + include Gitlab::Danger::Helper + + attr_reader :git + + def initialize(git:) + @git = git + end + end + + let(:teammate_json) do + <<~JSON + [ + { + "username": "in-gitlab-ce", + "name": "CE maintainer", + "projects":{ "gitlab-ce": "maintainer backend" } + }, + { + "username": "in-gitlab-ee", + "name": "EE reviewer", + "projects":{ "gitlab-ee": "reviewer frontend" } + } + ] + JSON + end + + let(:ce_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ce' && + teammate.name == 'CE maintainer' && + teammate.projects == { 'gitlab-ce' => 'maintainer backend' } + end + end + + let(:ee_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ee' && + teammate.name == 'EE reviewer' && + teammate.projects == { 'gitlab-ee' => 'reviewer frontend' } + end + end + + let(:fake_git) { double('fake-git') } + + subject(:helper) { FakeDanger.new(git: fake_git) } + + describe '#all_changed_files' do + subject { helper.all_changed_files } + + it 'interprets a list of changes from the danger git plugin' do + expect(fake_git).to receive(:added_files) { %w[a b c.old] } + expect(fake_git).to receive(:modified_files) { %w[d e] } + expect(fake_git) + .to receive(:renamed_files) + .at_least(:once) + .and_return([{ before: 'c.old', after: 'c.new' }]) + + is_expected.to contain_exactly('a', 'b', 'c.new', 'd', 'e') + end + end + + describe '#ee?' do + subject { helper.ee? } + + it 'returns true if CI_PROJECT_NAME if set to gitlab-ee' do + stub_env('CI_PROJECT_NAME', 'gitlab-ee') + expect(File).not_to receive(:exist?) + + is_expected.to be_truthy + end + + it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do + stub_env('CI_PROJECT_NAME', 'something else') + expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { true } + + is_expected.to be_truthy + end + + it 'returns true if CHANGELOG-EE.md exists' do + stub_env('CI_PROJECT_NAME', nil) + expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { true } + + is_expected.to be_truthy + end + + it "returns false if CHANGELOG-EE.md doesn't exist" do + stub_env('CI_PROJECT_NAME', nil) + expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { false } + + is_expected.to be_falsy + end + end + + describe '#project_name' do + subject { helper.project_name } + + it 'returns gitlab-ee if ee? returns true' do + expect(helper).to receive(:ee?) { true } + + is_expected.to eq('gitlab-ee') + end + + it 'returns gitlab-ce if ee? returns false' do + expect(helper).to receive(:ee?) { false } + + is_expected.to eq('gitlab-ce') + end + end + + describe '#team' do + subject(:team) { helper.team } + + context 'HTTP failure' do + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(status: 404) + end + + it 'raises a pretty error' do + expect { team }.to raise_error(/Failed to read/) + end + end + + context 'JSON failure' do + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(body: 'INVALID JSON') + end + + it 'raises a pretty error' do + expect { team }.to raise_error(/Failed to parse/) + end + end + + context 'success' do + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(body: teammate_json) + end + + it 'returns an array of teammates' do + is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher) + end + + it 'memoizes the result' do + expect(team.object_id).to eq(helper.team.object_id) + end + end + end + + describe '#project_team' do + subject { helper.project_team } + + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(body: teammate_json) + end + + it 'filters team by project_name' do + expect(helper) + .to receive(:project_name) + .at_least(:once) + .and_return('gitlab-ce') + + is_expected.to contain_exactly(ce_teammate_matcher) + end + end + + describe '#changes_by_category' do + it 'categorizes changed files' do + expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] } + allow(fake_git).to receive(:modified_files) { [] } + allow(fake_git).to receive(:renamed_files) { [] } + + expect(helper.changes_by_category).to eq( + backend: %w[foo.rb], + database: %w[db/foo], + docs: %w[foo.md], + frontend: %w[foo.js], + none: %w[ee/changelogs/foo.yml], + qa: %w[qa/foo], + unknown: %w[foo] + ) + end + end + + describe '#category_for_file' do + where(:path, :expected_category) do + 'doc/foo' | :docs + 'CONTRIBUTING.md' | :docs + 'LICENSE' | :docs + 'MAINTENANCE.md' | :docs + 'PHILOSOPHY.md' | :docs + 'PROCESS.md' | :docs + 'README.md' | :docs + + 'ee/doc/foo' | :unknown + 'ee/README' | :unknown + + 'app/assets/foo' | :frontend + 'app/views/foo' | :frontend + 'public/foo' | :frontend + 'spec/javascripts/foo' | :frontend + 'vendor/assets/foo' | :frontend + 'jest.config.js' | :frontend + 'package.json' | :frontend + 'yarn.lock' | :frontend + + 'ee/app/assets/foo' | :frontend + 'ee/app/views/foo' | :frontend + 'ee/spec/javascripts/foo' | :frontend + + 'app/models/foo' | :backend + 'bin/foo' | :backend + 'config/foo' | :backend + 'danger/foo' | :backend + 'lib/foo' | :backend + 'rubocop/foo' | :backend + 'scripts/foo' | :backend + 'spec/foo' | :backend + 'spec/foo/bar' | :backend + + 'ee/app/foo' | :backend + 'ee/bin/foo' | :backend + 'ee/spec/foo' | :backend + 'ee/spec/foo/bar' | :backend + + 'generator_templates/foo' | :backend + 'vendor/languages.yml' | :backend + 'vendor/licenses.csv' | :backend + + 'Dangerfile' | :backend + 'Gemfile' | :backend + 'Gemfile.lock' | :backend + 'Procfile' | :backend + 'Rakefile' | :backend + '.gitlab-ci.yml' | :backend + 'FOO_VERSION' | :backend + + 'ee/FOO_VERSION' | :unknown + + 'db/foo' | :database + 'qa/foo' | :qa + + 'ee/db/foo' | :database + 'ee/qa/foo' | :qa + + 'changelogs/foo' | :none + 'ee/changelogs/foo' | :none + + 'FOO' | :unknown + 'foo' | :unknown + + 'foo/bar.rb' | :backend + 'foo/bar.js' | :frontend + 'foo/bar.txt' | :docs + 'foo/bar.md' | :docs + end + + with_them do + subject { helper.category_for_file(path) } + + it { is_expected.to eq(expected_category) } + end + end + + describe '#label_for_category' do + where(:category, :expected_label) do + :backend | '~backend' + :database | '~database' + :docs | '~Documentation' + :foo | '~foo' + :frontend | '~frontend' + :none | '' + :qa | '~QA' + end + + with_them do + subject { helper.label_for_category(category) } + + it { is_expected.to eq(expected_label) } + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cf9e0cccc71..8a9e78ba3c3 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -283,6 +283,96 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#diverging_commit_count' do + it 'counts 0 for the same branch' do + expect(repository.diverging_commit_count('master', 'master', max_count: 1000)).to eq([0, 0]) + end + + context 'max count does not truncate results' do + where(:left, :right, :expected) do + 1 | 1 | [1, 1] + 4 | 4 | [4, 4] + 2 | 2 | [2, 2] + 2 | 4 | [2, 4] + 4 | 2 | [4, 2] + 10 | 10 | [10, 10] + end + + with_them do + before do + repository.create_branch('left-branch', 'master') + repository.create_branch('right-branch', 'master') + + left.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') + end + + right.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') + end + end + + after do + repository.delete_branch('left-branch') + repository.delete_branch('right-branch') + end + + it 'returns the correct count bounding at max_count' do + branch_a_sha = repository_rugged.branches['left-branch'].target.oid + branch_b_sha = repository_rugged.branches['right-branch'].target.oid + + count = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: 1000) + + expect(count).to eq(expected) + end + end + end + + context 'max count truncates results' do + where(:left, :right, :max_count) do + 1 | 1 | 1 + 4 | 4 | 4 + 2 | 2 | 3 + 2 | 4 | 3 + 4 | 2 | 5 + 10 | 10 | 10 + end + + with_them do + before do + repository.create_branch('left-branch', 'master') + repository.create_branch('right-branch', 'master') + + left.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') + end + + right.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') + end + end + + after do + repository.delete_branch('left-branch') + repository.delete_branch('right-branch') + end + + it 'returns the correct count bounding at max_count' do + branch_a_sha = repository_rugged.branches['left-branch'].target.oid + branch_b_sha = repository_rugged.branches['right-branch'].target.oid + + results = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: max_count) + + expect(results[0] + results[1]).to eq(max_count) + end + end + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :diverging_commit_count do + subject { repository.diverging_commit_count('master', 'master', max_count: 1000) } + end + end + describe '#has_local_branches?' do context 'check for local branches' do it { expect(repository.has_local_branches?).to eq(true) } diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 0f21b8843b6..15e59718dce 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -89,7 +89,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi description: 'This is my pull request', source_project_id: project.id, target_project_id: project.id, - source_branch: 'alice:feature', + source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, milestone_id: milestone.id, @@ -134,7 +134,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi description: "*Created by: alice*\n\nThis is my pull request", source_project_id: project.id, target_project_id: project.id, - source_branch: 'alice:feature', + source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, milestone_id: milestone.id, @@ -259,6 +259,40 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi .and_return(user.id) end + it 'does not create the source branch if merge request is merged' do + mr, exists = importer.create_merge_request + + importer.insert_git_data(mr, exists) + + expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end + + it 'creates the source branch if merge request is open' do + mr, exists = importer.create_merge_request + mr.state = 'opened' + mr.save + + importer.insert_git_data(mr, exists) + + expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end + + it 'ignores Git errors when creating a branch' do + mr, exists = importer.create_merge_request + mr.state = 'opened' + mr.save + + expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError) + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + importer.insert_git_data(mr, exists) + + expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end + it 'creates the merge request diffs' do mr, exists = importer.create_merge_request diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb index 33f6ff0ae6a..d478e5ae899 100644 --- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb +++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb @@ -238,7 +238,7 @@ describe Gitlab::GithubImport::Representation::PullRequest do target_repository_id: 2 ) - expect(pr.formatted_source_branch).to eq('foo:branch') + expect(pr.formatted_source_branch).to eq('github/fork/foo/branch') end end diff --git a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb new file mode 100644 index 00000000000..cf3a8bcc8b4 --- /dev/null +++ b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::Authorize::Instrumentation do + describe '#build_checker' do + let(:current_user) { double(:current_user) } + let(:abilities) { [double(:first_ability), double(:last_ability)] } + + let(:checker) do + described_class.new.__send__(:build_checker, current_user, abilities) + end + + it 'returns a checker which checks for a single object' do + object = double(:object) + + abilities.each do |ability| + spy_ability_check_for(ability, object, passed: true) + end + + expect(checker.call(object)).to eq(object) + end + + it 'returns a checker which checks for all objects' do + objects = [double(:first), double(:last)] + + abilities.each do |ability| + objects.each do |object| + spy_ability_check_for(ability, object, passed: true) + end + end + + expect(checker.call(objects)).to eq(objects) + end + + context 'when some objects would not pass the check' do + it 'returns nil when it is single object' do + disallowed = double(:object) + + spy_ability_check_for(abilities.first, disallowed, passed: false) + + expect(checker.call(disallowed)).to be_nil + end + + it 'returns only objects which passed when there are more than one' do + allowed = double(:allowed) + disallowed = double(:disallowed) + + spy_ability_check_for(abilities.first, disallowed, passed: false) + + abilities.each do |ability| + spy_ability_check_for(ability, allowed, passed: true) + end + + expect(checker.call([disallowed, allowed])) + .to contain_exactly(allowed) + end + end + + def spy_ability_check_for(ability, object, passed: true) + expect(Ability) + .to receive(:allowed?) + .with(current_user, ability, object) + .and_return(passed) + end + end +end diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb index f2d750c6595..2c288cff6ef 100644 --- a/spec/lib/gitlab/import_export/shared_spec.rb +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -14,6 +14,16 @@ describe Gitlab::ImportExport::Shared do expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]']) end + it 'updates the import JID' do + import_state = create(:import_state, project: project, jid: 'jid-test') + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:error).with(hash_including(import_jid: import_state.jid)) + end + + subject.error(error) + end + it 'calls the error logger with the full message' do expect(subject).to receive(:log_error).with(hash_including(message: error.message)) diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index 1ec1ba19e39..8961ecc4be0 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -4,10 +4,8 @@ require 'spec_helper' describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do describe '#token' do - shared_examples 'an LFS token generator' do + shared_examples 'a valid LFS token' do it 'returns a computed token' do - expect(Settings).to receive(:attr_encrypted_db_key_base).and_return('fbnbv6hdjweo53qka7kza8v8swxc413c05pb51qgtfte0bygh1p2e508468hfsn5ntmjcyiz7h1d92ashpet5pkdyejg7g8or3yryhuso4h8o5c73h429d9c3r6bjnet').twice - token = lfs_token.token expect(token).not_to be_nil @@ -20,11 +18,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:user, username: 'test_user_lfs_1') } let(:lfs_token) { described_class.new(actor) } - before do - allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO') - end - - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq(actor.username) @@ -40,11 +34,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:key, user: user) } let(:lfs_token) { described_class.new(actor) } - before do - allow(user).to receive(:encrypted_password).and_return('$2a$04$C1GAMKsOKouEbhKy2JQoe./3LwOfQAZc.hC8zW2u/wt8xgukvnlV.') - end - - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq(user.username) @@ -65,7 +55,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do allow(actor).to receive(:id).and_return(actor_id) end - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq("lfs+deploy-key-#{actor_id}") @@ -87,10 +77,6 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:user, username: 'test_user_lfs_1') } let(:lfs_token) { described_class.new(actor) } - before do - allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO') - end - context 'for an HMAC token' do before do # We're not interested in testing LegacyRedisDeviseToken here @@ -240,4 +226,18 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do end end end + + describe '#authentication_payload' do + it 'returns a Hash designed for gitlab-shell' do + actor = create(:user) + lfs_token = described_class.new(actor) + repo_http_path = 'http://localhost/user/repo.git' + authentication_payload = lfs_token.authentication_payload(repo_http_path) + + expect(authentication_payload[:username]).to eq(actor.username) + expect(authentication_payload[:repository_http_path]).to eq(repo_http_path) + expect(authentication_payload[:lfs_token]).to be_a String + expect(authentication_payload[:expires_in]).to eq(described_class::DEFAULT_EXPIRE_TIME) + end + end end diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb index 25146860615..7fe39dd5a96 100644 --- a/spec/lib/gitlab/sql/recursive_cte_spec.rb +++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb @@ -31,6 +31,15 @@ describe Gitlab::SQL::RecursiveCTE, :postgresql do expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}") end + + it 'replaces dots with an underscore' do + table = Arel::Table.new('gitlab.kittens') + + source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name) + alias_name = ActiveRecord::Base.connection.quote_table_name(:gitlab_kittens) + + expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}") + end end describe '#apply_to' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 4f5993ba226..d3eae80cc56 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -124,9 +124,15 @@ describe Gitlab::UsageData do todos uploads web_hooks + user_preferences )) end + it 'does not gather user preferences usage data when the feature is disabled' do + stub_feature_flags(group_overview_security_dashboard: false) + expect(subject[:counts].keys).not_to include(:user_preferences) + end + it 'gathers projects data correctly' do count_data = subject[:counts] diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 72a0df96a80..460b5c8cd31 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1172,8 +1172,26 @@ describe Ci::Pipeline, :mailer do pipeline.update_column(:before_sha, Gitlab::Git::BLANK_SHA) end - it 'raises an error' do - expect { pipeline.modified_paths }.to raise_error(ArgumentError) + it 'returns nil' do + expect(pipeline.modified_paths).to be_nil + end + end + + context 'when source is merge request' do + let(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'returns merge request modified paths' do + expect(pipeline.modified_paths).to match(merge_request.modified_paths) end end end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index 64f6d9c8bb4..f16eff92167 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -3,16 +3,17 @@ require 'rails_helper' describe Clusters::Applications::Helm do include_examples 'cluster application core specs', :clusters_applications_helm - describe '.installed' do - subject { described_class.installed } + describe '.available' do + subject { described_class.available } let!(:installed_cluster) { create(:clusters_applications_helm, :installed) } + let!(:updated_cluster) { create(:clusters_applications_helm, :updated) } before do create(:clusters_applications_helm, :errored) end - it { is_expected.to contain_exactly(installed_cluster) } + it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } end describe '#issue_client_cert' do diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 03ca18c6943..d5fd42509a3 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -16,18 +16,6 @@ describe Clusters::Applications::Ingress do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) end - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_ingress, :installed) } - - before do - create(:clusters_applications_ingress, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe '#make_installed!' do before do application.make_installed! diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index cd29e0d4f53..006b922ab27 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -24,30 +24,6 @@ describe Clusters::Applications::Knative do it { expect(knative_no_rbac).to be_not_installable } end - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_knative, :installed) } - - before do - create(:clusters_applications_knative, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - - describe '#make_installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_knative, :installed) } - - before do - create(:clusters_applications_knative, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe 'make_installed with external_ip' do before do application.make_installed! diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index caf59b0fc31..2250ef301aa 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -9,18 +9,6 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application helm specs', :clusters_applications_prometheus include_examples 'cluster application initial status specs' - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_prometheus, :installed) } - - before do - create(:clusters_applications_prometheus, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe 'transition to installed' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } @@ -192,7 +180,7 @@ describe Clusters::Applications::Prometheus do end context 'with knative installed' do - let(:knative) { create(:clusters_applications_knative, :installed ) } + let(:knative) { create(:clusters_applications_knative, :updated ) } let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) } subject { prometheus.install_command } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 38758ff97bc..5e5b25cbf8a 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -11,18 +11,6 @@ describe Clusters::Applications::Runner do it { is_expected.to belong_to(:runner) } - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_runner, :installed) } - - before do - create(:clusters_applications_runner, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe '#install_command' do let(:kubeclient) { double('kubernetes client') } let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 92ce2b0999a..3feed4e9718 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -265,12 +265,12 @@ describe Clusters::Cluster do it { is_expected.to be_valid } end - context 'when cluster has an invalid domain' do - let(:cluster) { build(:cluster, domain: 'not-valid-domain') } + context 'when cluster is not a valid hostname' do + let(:cluster) { build(:cluster, domain: 'http://not.a.valid.hostname') } it 'should add an error on domain' do expect(subject).not_to be_valid - expect(subject.errors[:domain].first).to eq('is not a fully qualified domain name') + expect(subject.errors[:domain].first).to eq('contains invalid characters (valid characters: [a-z0-9\\-])') end end diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb index 12e59b35428..0f5d03ff458 100644 --- a/spec/models/commit_collection_spec.rb +++ b/spec/models/commit_collection_spec.rb @@ -12,26 +12,26 @@ describe CommitCollection do end end - describe '.committers' do + describe '.authors' do it 'returns a relation of users when users are found' do - user = create(:user, email: commit.committer_email.upcase) + user = create(:user, email: commit.author_email.upcase) collection = described_class.new(project, [commit]) - expect(collection.committers).to contain_exactly(user) + expect(collection.authors).to contain_exactly(user) end - it 'returns empty array when committers cannot be found' do + it 'returns empty array when authors cannot be found' do collection = described_class.new(project, [commit]) - expect(collection.committers).to be_empty + expect(collection.authors).to be_empty end it 'excludes authors of merge commits' do commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98") - create(:user, email: commit.committer_email.upcase) + create(:user, email: commit.author_email.upcase) collection = described_class.new(project, [commit]) - expect(collection.committers).to be_empty + expect(collection.authors).to be_empty end end diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 97a4c212f1c..03ae45e6b17 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -25,7 +25,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do def result with_reactive_cache do |data| - data / 2 + data end end end @@ -64,7 +64,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do stub_reactive_cache(instance, 4) end - it { is_expected.to eq(2) } + it { is_expected.to eq(4) } it 'does not enqueue a background worker' do expect(ReactiveCachingWorker).not_to receive(:perform_async) @@ -94,6 +94,14 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end end end + + context 'when cache contains non-nil but blank value' do + before do + stub_reactive_cache(instance, false) + end + + it { is_expected.to eq(false) } + end end describe '#clear_reactive_cache!' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index afa87b8a62d..82a853a23b9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -435,6 +435,7 @@ describe MergeRequest do it 'does not cache issues from external trackers' do issue = ExternalIssue.new('JIRA-123', subject.project) commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") + allow(subject).to receive(:commits).and_return([commit]) expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error @@ -1023,23 +1024,23 @@ describe MergeRequest do end end - describe '#committers' do - it 'returns all the committers of every commit in the merge request' do - users = subject.commits.map(&:committer_email).uniq.map do |email| + describe '#commit_authors' do + it 'returns all the authors of every commit in the merge request' do + users = subject.commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end - expect(subject.committers).to match_array(users) + expect(subject.commit_authors).to match_array(users) end - it 'returns an empty array if no committer is associated with a user' do - expect(subject.committers).to be_empty + it 'returns an empty array if no author is associated with a user' do + expect(subject.commit_authors).to be_empty end end describe '#authors' do - it 'returns a list with all the committers in the merge request and author' do - users = subject.commits.map(&:committer_email).uniq.map do |email| + it 'returns a list with all the commit authors in the merge request and author' do + users = subject.commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end @@ -2604,8 +2605,9 @@ describe MergeRequest do let!(:first_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) } let!(:last_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) } + let!(:last_pipeline_with_other_ref) { create(:ci_pipeline_without_jobs, pipeline_arguments.merge(ref: 'other')) } - it 'returns latest pipeline' do + it 'returns latest pipeline for the target branch' do expect(merge_request.base_pipeline).to eq(last_pipeline) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7b364395faf..1f9088c2e6b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -4602,6 +4602,21 @@ describe Project do end end + describe '#has_pool_repsitory?' do + it 'returns false when it does not have a pool repository' do + subject = create(:project, :repository) + + expect(subject.has_pool_repository?).to be false + end + + it 'returns true when it has a pool repository' do + pool = create(:pool_repository, :ready) + subject = create(:project, :repository, pool_repository: pool) + + expect(subject.has_pool_repository?).to be true + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/releases/link_spec.rb b/spec/models/releases/link_spec.rb index 06ed1438688..4dd26c976cc 100644 --- a/spec/models/releases/link_spec.rb +++ b/spec/models/releases/link_spec.rb @@ -77,4 +77,28 @@ describe Releases::Link do it { is_expected.to be_truthy } end + + describe 'supported protocols' do + where(:protocol) do + %w(http https ftp) + end + + with_them do + let(:link) { build(:release_link, url: protocol + '://assets.com/download') } + + it 'will be valid' do + expect(link).to be_valid + end + end + end + + describe 'unsupported protocol' do + context 'for torrent' do + let(:link) { build(:release_link, url: 'torrent://assets.com/download') } + + it 'will be invalid' do + expect(link).to be_invalid + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 78477ab0a5a..1edd8e69b8f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -925,6 +925,21 @@ describe User do expect(user.manageable_groups).to contain_exactly(group, subgroup) end end + + describe '#manageable_groups_with_routes' do + it 'eager loads routes from manageable groups' do + control_count = + ActiveRecord::QueryRecorder.new(skip_cached: false) do + user.manageable_groups_with_routes.map(&:route) + end.count + + create(:group, parent: subgroup) + + expect do + user.manageable_groups_with_routes.map(&:route) + end.not_to exceed_all_query_limit(control_count) + end + end end end diff --git a/spec/policies/board_policy_spec.rb b/spec/policies/board_policy_spec.rb new file mode 100644 index 00000000000..4b76d65ef69 --- /dev/null +++ b/spec/policies/board_policy_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardPolicy do + let(:user) { create(:user) } + let(:project) { create(:project, :private) } + let(:group) { create(:group, :private) } + let(:group_board) { create(:board, group: group) } + let(:project_board) { create(:board, project: project) } + + let(:board_permissions) do + [ + :read_parent, + :read_milestone, + :read_issue + ] + end + + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end + + context 'group board' do + subject { described_class.new(user, group_board) } + + context 'user has access' do + before do + group.add_developer(user) + end + + it do + expect_allowed(*board_permissions) + end + end + + context 'user does not have access' do + it do + expect_disallowed(*board_permissions) + end + end + end + + context 'project board' do + subject { described_class.new(user, project_board) } + + context 'user has access' do + before do + project.add_developer(user) + end + + it do + expect_allowed(*board_permissions) + end + end + + context 'user does not have access' do + it do + expect_disallowed(*board_permissions) + end + end + end +end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 6b9bc6eda6a..066f1d6862a 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -237,7 +237,7 @@ describe API::Commits do end describe 'create' do - let(:message) { 'Created file' } + let(:message) { 'Created a new file with a very very looooooooooooooooooooooooooooooooooooooooooooooong commit message' } let(:invalid_c_params) do { branch: 'master', @@ -1457,4 +1457,42 @@ describe API::Commits do expect(response).to have_gitlab_http_status(404) end end + + describe 'GET /projects/:id/repository/commits/:sha/signature' do + let!(:project) { create(:project, :repository, :public) } + let(:project_id) { project.id } + let(:commit_id) { project.repository.commit.id } + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/signature" } + + context 'when commit does not exist' do + let(:commit_id) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Commit Not Found' } + end + end + + context 'unsigned commit' do + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 GPG Signature Not Found'} + end + end + + context 'signed commit' do + let(:commit) { project.repository.commit(GpgHelpers::SIGNED_COMMIT_SHA) } + let(:commit_id) { commit.id } + + it 'returns correct JSON' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['gpg_key_id']).to eq(commit.signature.gpg_key_id) + expect(json_response['gpg_key_subkey_id']).to eq(commit.signature.gpg_key_subkey_id) + expect(json_response['gpg_key_primary_keyid']).to eq(commit.signature.gpg_key_primary_keyid) + expect(json_response['verification_status']).to eq(commit.signature.verification_status) + end + end + end end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 355336ad7e2..c2934430821 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -56,4 +56,38 @@ describe 'getting an issue list for a project' do expect(issues_data).to eq [] end end + + context 'when there is a confidential issue' do + let!(:confidential_issue) do + create(:issue, :confidential, project: project) + end + + context 'when the user cannot see confidential issues' do + it 'returns issues without confidential issues' do + post_graphql(query, current_user: current_user) + + expect(issues_data.size).to eq(2) + + issues_data.each do |issue| + expect(issue.dig('node', 'confidential')).to eq(false) + end + end + end + + context 'when the user can see confidential issues' do + it 'returns issues with confidential issues' do + project.add_developer(current_user) + + post_graphql(query, current_user: current_user) + + expect(issues_data.size).to eq(3) + + confidentials = issues_data.map do |issue| + issue.dig('node', 'confidential') + end + + expect(confidentials).to eq([true, false, false]) + end + end + end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 6a943b5237a..cd85151ec1b 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -167,6 +167,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response['username']).to eq(user.username) expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) + expect(json_response['expires_in']).to eq(Gitlab::LfsToken::DEFAULT_EXPIRE_TIME) expect(Gitlab::LfsToken.new(key).token_valid?(json_response['lfs_token'])).to be_truthy end @@ -324,6 +325,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') + expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to be_nil end @@ -336,6 +338,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') + expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to eql(Date.today) end @@ -349,6 +352,7 @@ describe API::Internal do expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) @@ -368,6 +372,7 @@ describe API::Internal do expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 49eea2e362b..518181e4d93 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -20,9 +20,9 @@ describe API::Labels do create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) expected_keys = %w( - id name color description + id name color text_color description open_issues_count closed_issues_count open_merge_requests_count - subscribed priority + subscribed priority is_project_label ) get api("/projects/#{project.id}/labels", user) @@ -43,27 +43,33 @@ describe API::Labels do expect(label1_response['open_merge_requests_count']).to eq(0) expect(label1_response['name']).to eq(label1.name) expect(label1_response['color']).to be_present + expect(label1_response['text_color']).to be_present expect(label1_response['description']).to be_nil expect(label1_response['priority']).to be_nil expect(label1_response['subscribed']).to be_falsey + expect(label1_response['is_project_label']).to be_truthy expect(group_label_response['open_issues_count']).to eq(1) expect(group_label_response['closed_issues_count']).to eq(0) expect(group_label_response['open_merge_requests_count']).to eq(0) expect(group_label_response['name']).to eq(group_label.name) expect(group_label_response['color']).to be_present + expect(group_label_response['text_color']).to be_present expect(group_label_response['description']).to be_nil expect(group_label_response['priority']).to be_nil expect(group_label_response['subscribed']).to be_falsey + expect(group_label_response['is_project_label']).to be_falsey expect(priority_label_response['open_issues_count']).to eq(0) expect(priority_label_response['closed_issues_count']).to eq(0) expect(priority_label_response['open_merge_requests_count']).to eq(1) expect(priority_label_response['name']).to eq(priority_label.name) expect(priority_label_response['color']).to be_present + expect(priority_label_response['text_color']).to be_present expect(priority_label_response['description']).to be_nil expect(priority_label_response['priority']).to eq(3) expect(priority_label_response['subscribed']).to be_falsey + expect(priority_label_response['is_project_label']).to be_truthy end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 0f5f6e38819..b8426126bc6 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -372,6 +372,7 @@ describe API::MergeRequests do expect(json_response['force_close_merge_request']).to be_falsy expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size) expect(json_response['merge_error']).to eq(merge_request.merge_error) + expect(json_response['user']['can_merge']).to be_truthy expect(json_response).not_to include('rebase_in_progress') end @@ -499,6 +500,15 @@ describe API::MergeRequests do expect(json_response['allow_maintainer_to_push']).to be_truthy end end + + it 'indicates if a user cannot merge the MR' do + user2 = create(:user) + project.add_reporter(user2) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user2) + + expect(json_response['user']['can_merge']).to be_falsy + end end describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 6109829aad1..d1b58aac104 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -100,6 +100,8 @@ describe API::Wikis do shared_examples_for 'updates wiki page' do it 'updates the wiki page' do + put(api(url, user), params: payload) + expect(response).to have_gitlab_http_status(200) expect(json_response.size).to eq(4) expect(json_response.keys).to match_array(expected_keys_with_content) @@ -107,6 +109,16 @@ describe API::Wikis do expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) expect(json_response['title']).to eq(payload[:title]) end + + [:title, :content, :format].each do |part| + it "it updates with wiki with missing #{part}" do + payload.delete(part) + + put(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(200) + end + end end shared_examples_for '403 Forbidden' do @@ -528,8 +540,6 @@ describe API::Wikis do context 'when user is developer' do before do project.add_developer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -537,6 +547,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -544,8 +558,6 @@ describe API::Wikis do context 'when user is maintainer' do before do project.add_maintainer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -553,6 +565,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -572,8 +588,6 @@ describe API::Wikis do context 'when user is developer' do before do project.add_developer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -581,6 +595,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -588,8 +606,6 @@ describe API::Wikis do context 'when user is maintainer' do before do project.add_maintainer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -597,6 +613,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -605,10 +625,6 @@ describe API::Wikis do context 'when wiki belongs to a group project' do let(:project) { create(:project, :wiki_repo, namespace: group) } - before do - put(api(url, user), params: payload) - end - include_examples 'updates wiki page' end end diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb index 78ff9c6e6fd..106f92082e4 100644 --- a/spec/routing/import_routing_spec.rb +++ b/spec/routing/import_routing_spec.rb @@ -23,6 +23,11 @@ require 'spec_helper' # end shared_examples 'importer routing' do let(:except_actions) { [] } + let(:is_realtime) { false } + + before do + except_actions.push(is_realtime ? :jobs : :realtime_changes) + end it 'to #create' do expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create) @@ -43,17 +48,22 @@ shared_examples 'importer routing' do it 'to #jobs' do expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs) end + + it 'to #realtime_changes' do + expect(get("/import/#{provider}/realtime_changes")).to route_to("import/#{provider}#realtime_changes") unless except_actions.include?(:realtime_changes) + end end # personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token # status_import_github GET /import/github/status(.:format) import/github#status # callback_import_github GET /import/github/callback(.:format) import/github#callback -# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs +# realtime_changes_import_github GET /import/github/realtime_changes(.:format) import/github#jobs # import_github POST /import/github(.:format) import/github#create # new_import_github GET /import/github/new(.:format) import/github#new describe Import::GithubController, 'routing' do it_behaves_like 'importer routing' do let(:provider) { 'github' } + let(:is_realtime) { true } end it 'to #personal_access_token' do @@ -63,13 +73,14 @@ end # personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token # status_import_gitea GET /import/gitea/status(.:format) import/gitea#status -# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs +# realtime_changes_import_gitea GET /import/gitea/realtime_changes(.:format) import/gitea#jobs # import_gitea POST /import/gitea(.:format) import/gitea#create # new_import_gitea GET /import/gitea/new(.:format) import/gitea#new describe Import::GiteaController, 'routing' do it_behaves_like 'importer routing' do let(:except_actions) { [:callback] } let(:provider) { 'gitea' } + let(:is_realtime) { true } end it 'to #personal_access_token' do diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb index 073c13c2cbb..92b649f5b6c 100644 --- a/spec/serializers/diff_file_entity_spec.rb +++ b/spec/serializers/diff_file_entity_spec.rb @@ -22,7 +22,7 @@ describe DiffFileEntity do let(:request) { EntityRequest.new(project: project, current_user: user) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) } - let(:exposed_urls) { %i(load_collapsed_diff_url edit_path view_path context_lines_path) } + let(:exposed_urls) { %i(edit_path view_path context_lines_path) } it_behaves_like 'diff file entity' @@ -38,6 +38,12 @@ describe DiffFileEntity do expect(response[attribute]).to include(merge_request.target_project.to_param) end end + + it 'exposes load_collapsed_diff_url if the file viewer is collapsed' do + allow(diff_file.viewer).to receive(:collapsed?) { true } + + expect(subject).to include(:load_collapsed_diff_url) + end end context '#parallel_diff_lines' do diff --git a/spec/serializers/namespace_basic_entity_spec.rb b/spec/serializers/namespace_basic_entity_spec.rb new file mode 100644 index 00000000000..f8b71ceb9f3 --- /dev/null +++ b/spec/serializers/namespace_basic_entity_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NamespaceBasicEntity do + set(:group) { create(:group) } + let(:entity) do + described_class.represent(group) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'includes required fields' do + expect(subject).to include :id, :full_path + end + end +end diff --git a/spec/serializers/namespace_serializer_spec.rb b/spec/serializers/namespace_serializer_spec.rb new file mode 100644 index 00000000000..6e5bdd8c52d --- /dev/null +++ b/spec/serializers/namespace_serializer_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NamespaceSerializer do + it 'represents NamespaceBasicEntity entities' do + expect(described_class.entity_class).to eq(NamespaceBasicEntity) + end +end diff --git a/spec/serializers/project_import_entity_spec.rb b/spec/serializers/project_import_entity_spec.rb new file mode 100644 index 00000000000..e476da82729 --- /dev/null +++ b/spec/serializers/project_import_entity_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectImportEntity do + include ImportHelper + + set(:project) { create(:project, import_status: :started, import_source: 'namespace/project') } + let(:provider_url) { 'https://provider.com' } + let(:entity) { described_class.represent(project, provider_url: provider_url) } + + describe '#as_json' do + subject { entity.as_json } + + it 'includes required fields' do + expect(subject[:import_source]).to eq(project.import_source) + expect(subject[:import_status]).to eq(project.import_status) + expect(subject[:human_import_status_name]).to eq(project.human_import_status_name) + expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source])) + end + end +end diff --git a/spec/serializers/project_serializer_spec.rb b/spec/serializers/project_serializer_spec.rb new file mode 100644 index 00000000000..22f958fc17f --- /dev/null +++ b/spec/serializers/project_serializer_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectSerializer do + set(:project) { create(:project) } + let(:provider_url) { 'http://provider.com' } + + context 'when serializer option is :import' do + subject do + described_class.new.represent(project, serializer: :import, provider_url: provider_url) + end + + before do + allow(ProjectImportEntity).to receive(:represent) + end + + it 'represents with ProjectImportEntity' do + subject + + expect(ProjectImportEntity) + .to have_received(:represent) + .with(project, serializer: :import, provider_url: provider_url, request: an_instance_of(EntityRequest)) + end + end + + context 'when serializer option is omitted' do + subject do + described_class.new.represent(project) + end + + before do + allow(ProjectEntity).to receive(:represent) + end + + it 'represents with ProjectEntity' do + subject + + expect(ProjectEntity) + .to have_received(:represent) + .with(project, request: an_instance_of(EntityRequest)) + end + end +end diff --git a/spec/serializers/provider_repo_entity_spec.rb b/spec/serializers/provider_repo_entity_spec.rb new file mode 100644 index 00000000000..b67115bab10 --- /dev/null +++ b/spec/serializers/provider_repo_entity_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProviderRepoEntity do + include ImportHelper + + let(:provider_repo) { { id: 1, full_name: 'full/name', name: 'name', owner: { login: 'owner' } } } + let(:provider) { :github } + let(:provider_url) { 'https://github.com' } + let(:entity) { described_class.represent(provider_repo, provider: provider, provider_url: provider_url) } + + describe '#as_json' do + subject { entity.as_json } + + it 'includes requried fields' do + expect(subject[:id]).to eq(provider_repo[:id]) + expect(subject[:full_name]).to eq(provider_repo[:full_name]) + expect(subject[:owner_name]).to eq(provider_repo[:owner][:login]) + expect(subject[:sanitized_name]).to eq(sanitize_project_name(provider_repo[:name])) + expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, provider_repo[:full_name])) + end + end +end diff --git a/spec/serializers/provider_repo_serializer_spec.rb b/spec/serializers/provider_repo_serializer_spec.rb new file mode 100644 index 00000000000..f2be30c36d9 --- /dev/null +++ b/spec/serializers/provider_repo_serializer_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProviderRepoSerializer do + it 'represents ProviderRepoEntity entities' do + expect(described_class.entity_class).to eq(ProviderRepoEntity) + end +end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 48f1d696ff6..1645b67c329 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -311,7 +311,14 @@ describe Notes::CreateService do end it 'converts existing note to DiscussionNote' do - expect { subject }.to change { existing_note.reload.type }.from(nil).to('DiscussionNote') + expect do + existing_note + + Timecop.freeze(Time.now + 1.minute) { subject } + + existing_note.reload + end.to change { existing_note.type }.from(nil).to('DiscussionNote') + .and change { existing_note.updated_at } end end end diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb index 7c5480d382f..b1260cf740a 100644 --- a/spec/services/task_list_toggle_service_spec.rb +++ b/spec/services/task_list_toggle_service_spec.rb @@ -12,6 +12,10 @@ describe TaskListToggleService do 1. [X] Item 1 - [ ] Sub-item 1 + + - [ ] loose list + + with an embedded paragraph EOT end @@ -26,16 +30,22 @@ describe TaskListToggleService do </li> </ul> <p data-sourcepos="4:1-4:11" dir="auto">A paragraph</p> - <ol data-sourcepos="6:1-7:19" class="task-list" dir="auto"> - <li data-sourcepos="6:1-7:19" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled checked> Item 1 - <ul data-sourcepos="7:4-7:19" class="task-list"> - <li data-sourcepos="7:4-7:19" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> Sub-item 1 + <ol data-sourcepos="6:1-8:0" class="task-list" dir="auto"> + <li data-sourcepos="6:1-8:0" class="task-list-item"> + <input type="checkbox" class="task-list-item-checkbox" checked="" disabled=""> Item 1 + <ul data-sourcepos="7:4-8:0" class="task-list"> + <li data-sourcepos="7:4-8:0" class="task-list-item"> + <input type="checkbox" class="task-list-item-checkbox" disabled=""> Sub-item 1 </li> </ul> </li> </ol> + <ul data-sourcepos="9:1-11:28" class="task-list" dir="auto"> + <li data-sourcepos="9:1-11:28" class="task-list-item"> + <p data-sourcepos="9:3-9:16"><input type="checkbox" class="task-list-item-checkbox" disabled=""> loose list</p> + <p data-sourcepos="11:3-11:28">with an embedded paragraph</p> + </li> + </ul> EOT end @@ -59,6 +69,16 @@ describe TaskListToggleService do expect(toggler.updated_markdown_html).to include('disabled> Item 1') end + it 'checks task in loose list' do + toggler = described_class.new(markdown, markdown_html, + toggle_as_checked: true, + line_source: '- [ ] loose list', line_number: 9) + + expect(toggler.execute).to be_truthy + expect(toggler.updated_markdown.lines[8]).to eq "- [x] loose list\n" + expect(toggler.updated_markdown_html).to include('disabled checked> loose list') + end + it 'returns false if line_source does not match the text' do toggler = described_class.new(markdown, markdown_html, toggle_as_checked: false, diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index 719b4adf212..3c0a4ac8e18 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -26,6 +26,12 @@ describe Users::ActivityService do .from(last_activity_on) .to(Date.today) end + + it 'tries to obtain ExclusiveLease' do + expect(Gitlab::ExclusiveLease).to receive(:new).and_call_original + + subject.execute + end end context 'when a bad object is passed' do @@ -46,6 +52,12 @@ describe Users::ActivityService do it 'does not update last_activity_on' do expect { subject.execute }.not_to change(user, :last_activity_on) end + + it 'does not try to obtain ExclusiveLease' do + expect(Gitlab::ExclusiveLease).not_to receive(:new) + + subject.execute + end end context 'when in GitLab read-only instance' do diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index 697f999e4c4..5bb1269a19d 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -58,36 +58,54 @@ end shared_examples 'a GitHub-ish import controller: GET status' do let(:new_import_url) { public_send("new_import_#{provider}_url") } let(:user) { create(:user) } - let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } + let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) } let(:org) { OpenStruct.new(login: 'company') } - let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } - let(:extra_assign_expectations) { {} } + let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) } before do assign_session_token(provider) end - it "assigns variables" do - project = create(:project, import_type: provider, namespace: user.namespace) + it "returns variables for json request" do + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') + group = create(:group) + group.add_owner(user) stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) - get :status + get :status, format: :json - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([repo, org_repo]) - extra_assign_expectations.each do |key, value| - expect(assigns(key)).to eq(value) - end + expect(response).to have_gitlab_http_status(200) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo.id) + expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end it "does not show already added project" do - project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim') + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim') stub_client(repos: [repo], orgs: []) + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos")).to eq([]) + end + + it "touches the etag cache store" do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| + expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } + end + + get :status, format: :json + end + + it "requests provider repos list" do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + get :status - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([]) + expect(response).to have_gitlab_http_status(200) end it "handles an invalid access token" do @@ -100,13 +118,32 @@ shared_examples 'a GitHub-ish import controller: GET status' do expect(controller).to redirect_to(new_import_url) expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.") end + + it "does not produce N+1 database queries" do + stub_client(repos: [repo], orgs: []) + group_a = create(:group) + group_a.add_owner(user) + create(:project, :import_started, import_type: provider, namespace: user.namespace) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get :status, format: :json + end.count + + stub_client(repos: [repo, org_repo], orgs: []) + group_b = create(:group) + group_b.add_owner(user) + create(:project, :import_started, import_type: provider, namespace: user.namespace) + + expect { get :status, format: :json } + .not_to exceed_all_query_limit(control_count) + end end shared_examples 'a GitHub-ish import controller: POST create' do let(:user) { create(:user) } - let(:project) { create(:project) } let(:provider_username) { user.username } let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:project) { create(:project, import_type: provider, import_status: :finished, import_source: "#{provider_username}/vim") } let(:provider_repo) do OpenStruct.new( name: 'vim', @@ -145,6 +182,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(json_response['errors']).to eq('Name is invalid, Path is old') end + it "touches the etag cache store" do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| + expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } + end + + post :create, format: :json + end + context "when the repository owner is the provider user" do context "when the provider user and GitLab user's usernames match" do it "takes the current user's namespace" do @@ -351,7 +399,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create a new namespace under the user namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js } .not_to change { Namespace.count } @@ -365,7 +413,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not take the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js end @@ -373,7 +421,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js } .not_to change { Namespace.count } @@ -390,7 +438,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js end @@ -407,3 +455,20 @@ shared_examples 'a GitHub-ish import controller: POST create' do end end end + +shared_examples 'a GitHub-ish import controller: GET realtime_changes' do + let(:user) { create(:user) } + + before do + assign_session_token(provider) + end + + it 'sets a Poll-Interval header' do + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') + + get :realtime_changes + + expect(json_response).to eq([{ "id" => project.id, "import_status" => project.import_status }]) + expect(Integer(response.headers['Poll-Interval'])).to be > -1 + end +end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index ea3a03879c5..e468ee4676d 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -84,7 +84,7 @@ module GraphqlHelpers QUERY end - def all_graphql_fields_for(class_name) + def all_graphql_fields_for(class_name, parent_types = Set.new) type = GitlabSchema.types[class_name.to_s] return "" unless type @@ -92,8 +92,17 @@ module GraphqlHelpers # We can't guess arguments, so skip fields that require them next if required_arguments?(field) + singular_field_type = field_type(field) + + # If field type is the same as parent type, then we're hitting into + # mutual dependency. Break it from infinite recursion + next if parent_types.include?(singular_field_type) + if nested_fields?(field) - "#{name} { #{all_graphql_fields_for(field_type(field))} }" + fields = + all_graphql_fields_for(singular_field_type, parent_types | [type]) + + "#{name} { #{fields} }" else name end diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb index a575aa99b79..b76b53db0b9 100644 --- a/spec/support/helpers/reactive_caching_helpers.rb +++ b/spec/support/helpers/reactive_caching_helpers.rb @@ -10,7 +10,7 @@ module ReactiveCachingHelpers def stub_reactive_cache(subject = nil, data = nil, *qualifiers) allow(ReactiveCachingWorker).to receive(:perform_async) allow(ReactiveCachingWorker).to receive(:perform_in) - write_reactive_cache(subject, data, *qualifiers) if data + write_reactive_cache(subject, data, *qualifiers) unless data.nil? end def synchronous_reactive_cache(subject) diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index 4061a8d1bc9..48258692304 100644 --- a/spec/support/helpers/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb @@ -1,11 +1,30 @@ module StubFeatureFlags # Stub Feature flags with `flag_name: true/false` # - # @param [Hash] features where key is feature name and value is boolean whether enabled or not + # @param [Hash] features where key is feature name and value is boolean whether enabled or not. + # Alternatively, you can specify Hash to enable the flag on a specific thing. + # + # Examples + # - `stub_feature_flags(ci_live_trace: false)` ... Disable `ci_live_trace` + # feature flag globally. + # - `stub_feature_flags(ci_live_trace: { enabled: false, thing: project })` ... + # Disable `ci_live_trace` feature flag on the specified project. def stub_feature_flags(features) - features.each do |feature_name, enabled| - allow(Feature).to receive(:enabled?).with(feature_name, any_args) { enabled } - allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args) { enabled } + features.each do |feature_name, option| + if option.is_a?(Hash) + enabled, thing = option.values_at(:enabled, :thing) + else + enabled = option + thing = nil + end + + if thing + allow(Feature).to receive(:enabled?).with(feature_name, thing, any_args) { enabled } + allow(Feature).to receive(:enabled?).with(feature_name.to_s, thing, any_args) { enabled } + else + allow(Feature).to receive(:enabled?).with(feature_name, any_args) { enabled } + allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args) { enabled } + end end end end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index c96a65cb56a..b8c19cab0c4 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -9,6 +9,19 @@ shared_examples 'cluster application status specs' do |application_name| end end + describe '.available' do + subject { described_class.available } + + let!(:installed_cluster) { create(application_name, :installed) } + let!(:updated_cluster) { create(application_name, :updated) } + + before do + create(application_name, :errored) + end + + it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } + end + describe 'status state machine' do describe '#make_installing' do subject { create(application_name, :scheduled) } diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb index 1d11b855459..43033a2d256 100644 --- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb +++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb @@ -44,26 +44,6 @@ shared_examples_for 'model with uploads' do |supports_fileuploads| model_object.destroy end end - - describe 'destroy strategy depending on feature flag' do - let!(:upload) { create(:upload, uploader: FileUploader, model: model_object) } - - it 'does not destroy uploads by default' do - expect(model_object).to receive(:delete_uploads) - expect(model_object).not_to receive(:destroy_uploads) - - model_object.destroy - end - - it 'uses before destroy callback if feature flag is disabled' do - stub_feature_flags(fast_destroy_uploads: false) - - expect(model_object).to receive(:destroy_uploads) - expect(model_object).not_to receive(:delete_uploads) - - model_object.destroy - end - end end end end diff --git a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb index 1770308f789..96cb71be737 100644 --- a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb +++ b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb @@ -6,9 +6,9 @@ shared_examples 'diff file base entity' do :submodule_tree_url, :old_path_html, :new_path_html, :blob, :can_modify_blob, :file_hash, :file_path, :old_path, :new_path, - :collapsed, :text, :diff_refs, :stored_externally, + :viewer, :diff_refs, :stored_externally, :external_storage, :renamed_file, :deleted_file, - :mode_changed, :a_mode, :b_mode, :new_file) + :a_mode, :b_mode, :new_file) end # Converted diff files from GitHub import does not contain blob file @@ -30,9 +30,9 @@ shared_examples 'diff file entity' do it_behaves_like 'diff file base entity' it 'exposes correct attributes' do - expect(subject).to include(:too_large, :added_lines, :removed_lines, + expect(subject).to include(:added_lines, :removed_lines, :context_lines_path, :highlighted_diff_lines, - :parallel_diff_lines, :empty) + :parallel_diff_lines) end it 'includes viewer' do diff --git a/yarn.lock b/yarn.lock index 8bff9c59113..47df016b7a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.2.2": +"@babel/core@>=7.1.0", "@babel/core@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.2.tgz#07adba6dde27bb5ad8d8672f15fde3e08184a687" integrity sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw== @@ -653,10 +653,10 @@ eslint-plugin-promise "^4.0.1" eslint-plugin-vue "^5.0.0" -"@gitlab/svgs@^1.51.0": - version "1.51.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.51.0.tgz#1b608f68dfb74284401b1cbdb823440f6e8b0091" - integrity sha512-B1Wdhfy5ZClkHuaaCUUZyOBF8CFxxHqxGGhveRekOowtlMExa3tx+YkqNa5XPsEVMF6Aqnh8evQmmN4b+zrHVQ== +"@gitlab/svgs@^1.52.0": + version "1.52.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.52.0.tgz#870b0112a18b10d2cde5470a48b05025193cd207" + integrity sha512-xqDYIaSY1MJuCa7lRIDTTPoXn6x57So2qchxwmELE+SAJxlYlpYgDKCNWcGawhyTZRDZuG/qFBWp0sMeTQD//A== "@gitlab/ui@^2.0.2": version "2.0.2" @@ -674,6 +674,19 @@ vue "^2.5.21" vue-loader "^15.4.2" +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" + +"@nodelib/fs.stat@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" + integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== + "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" @@ -750,6 +763,28 @@ dependencies: source-map "^0.6.1" +"@types/unist@*", "@types/unist@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.2.tgz#5dc0a7f76809b7518c0df58689cd16a19bd751c6" + integrity sha512-iHI60IbyfQilNubmxsq4zqSjdynlmc2Q/QvH9kjzg9+CCYVVzq1O6tc7VBzSygIwnmOt07w80IG6HDQvjv3Liw== + +"@types/vfile-message@*": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-1.0.1.tgz#e1e9895cc6b36c462d4244e64e6d0b6eaf65355a" + integrity sha512-mlGER3Aqmq7bqR1tTTIVHq8KSAFFRyGbrxuM8C/H82g6k7r2fS+IMEkIu3D7JHzG10NvPdR8DNx0jr0pwpp4dA== + dependencies: + "@types/node" "*" + "@types/unist" "*" + +"@types/vfile@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/vfile/-/vfile-3.0.2.tgz#19c18cd232df11ce6fa6ad80259bc86c366b09b9" + integrity sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw== + dependencies: + "@types/node" "*" + "@types/unist" "*" + "@types/vfile-message" "*" + "@types/webpack@^4.4.19": version "4.4.23" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.23.tgz#059d6f4598cfd65ddee0e2db38317ef989696712" @@ -1012,10 +1047,10 @@ ajv-keywords@^3.1.0: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= -ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5, ajv@^6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61" - integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww== +ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5, ajv@^6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.1.tgz#a4d3683d74abc5670e75f0b16520f70a20ea8dc1" + integrity sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -1064,6 +1099,11 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +ansi-regex@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" + integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -1242,6 +1282,11 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + array-find@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8" @@ -1262,7 +1307,7 @@ array-slice@^0.2.3: resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= -array-union@^1.0.1: +array-union@^1.0.1, array-union@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= @@ -1364,6 +1409,18 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +autoprefixer@^9.0.0: + version "9.4.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.7.tgz#f997994f9a810eae47b38fa6d8a119772051c4ff" + integrity sha512-qS5wW6aXHkm53Y4z73tFGsUhmZu4aMPV9iHXYlF0c/wxjknXNHuj/1cIQb+6YH692DbJGGWcckAXX+VxKvahMA== + dependencies: + browserslist "^4.4.1" + caniuse-lite "^1.0.30000932" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.14" + postcss-value-parser "^3.3.1" + autosize@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.0.tgz#7a0599b1ba84d73bd7589b0d9da3870152c69237" @@ -1645,6 +1702,11 @@ backo2@1.0.2: resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= +bail@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.3.tgz#63cfb9ddbac829b02a3128cd53224be78e6c21a3" + integrity sha512-1X8CnjFVQ+a+KW36uBNMTU5s8+v5FzeqrP7hTG5aTb4aPreSbZJlhwPon9VKMuEVgV++JM+SQrALY3kr7eswdg== + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -1912,13 +1974,13 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.7.tgz#f1de479a6466ea47a0a26dcc725e7504817e624a" - integrity sha512-pWQv51Ynb0MNk9JGMCZ8VkM785/4MQNXiFYtPqI7EEP0TJO+/d/NqRVn1uiAN0DNbnlUSpL2sh16Kspasv3pUQ== +browserslist@^4.3.4, browserslist@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" + integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A== dependencies: - caniuse-lite "^1.0.30000925" - electron-to-chromium "^1.3.96" + caniuse-lite "^1.0.30000929" + electron-to-chromium "^1.3.103" node-releases "^1.1.3" bser@^2.0.0: @@ -1952,11 +2014,6 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -builtin-modules@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -2026,6 +2083,18 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -2033,6 +2102,13 @@ caller-path@^0.1.0: dependencies: callsites "^0.2.0" +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + callsite@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" @@ -2048,6 +2124,15 @@ callsites@^2.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= +camelcase-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" + integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= + dependencies: + camelcase "^4.1.0" + map-obj "^2.0.0" + quick-lru "^1.0.0" + camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -2058,10 +2143,10 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -caniuse-lite@^1.0.30000925: - version "1.0.30000927" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000927.tgz#114a9de4ff1e01f5790fe578ecd93421c7524665" - integrity sha512-ogq4NbUWf1uG/j66k0AmiO3GjqJAlQyF8n4w8a954cbCyFKmYGvRtgz6qkq2fWuduTXHibX7GyYL5Pg58Aks2g== +caniuse-lite@^1.0.30000929, caniuse-lite@^1.0.30000932: + version "1.0.30000936" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000936.tgz#5d33b118763988bf721b9b8ad436d0400e4a116b" + integrity sha512-orX4IdpbFhdNO7bTBhSbahp1EBpqzBc+qrvTRVUFfZgA4zta7TdM6PN5ZxkEUgDnz36m+PfWGcdX7AVfFWItJw== capture-exit@^1.2.0: version "1.2.0" @@ -2087,6 +2172,11 @@ catharsis@~0.8.9: dependencies: underscore-contrib "~0.3.0" +ccount@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.3.tgz#f1cec43f332e2ea5a569fd46f9f5bde4e6102aff" + integrity sha512-Jt9tIBkRc9POUof7QA/VwWd+58fKkEEfI+/t1/eOlxKM7ZhrczNzMFefge7Ai+39y1pR/pP6cI19guHy3FSLmw== + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2098,15 +2188,35 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" supports-color "^5.3.0" +character-entities-html4@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.2.tgz#c44fdde3ce66b52e8d321d6c1bf46101f0150610" + integrity sha512-sIrXwyna2+5b0eB9W149izTPJk/KkJTg6mEzDGibwBUkyH1SbDa+nf515Ppdi3MaH35lW0JFJDWeq9Luzes1Iw== + +character-entities-legacy@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz#7c6defb81648498222c9855309953d05f4d63a9c" + integrity sha512-9NB2VbXtXYWdXzqrvAHykE/f0QJxzaKIpZ5QzNZrrgQ7Iyxr2vnfS8fCBNVW9nUEZE0lo57nxKRqnzY/dKrwlA== + +character-entities@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.2.tgz#58c8f371c0774ef0ba9b2aca5f00d8f100e6e363" + integrity sha512-sMoHX6/nBiy3KKfC78dnEalnpn0Az0oSNvqUWYTtYrhRI5iUIYsROU48G+E+kMFQzqXaJ8kHJZ85n7y6/PHgwQ== + +character-reference-invalid@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz#21e421ad3d84055952dab4a43a04e73cd425d3ed" + integrity sha512-7I/xceXfKyUJmSAn/jw8ve/9DyOP7XxufNYLI9Px7CmsKgEUaZLUTax6nZxGQtaoiZCjpu6cHPj20xC/vqRReQ== + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" @@ -2265,6 +2375,14 @@ cliui@^4.0.0: strip-ansi "^4.0.0" wrap-ansi "^2.0.0" +clone-regexp@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-1.0.1.tgz#051805cd33173375d82118fc0918606da39fd60f" + integrity sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw== + dependencies: + is-regexp "^1.0.0" + is-supported-regexp-flag "^1.0.0" + clone-response@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" @@ -2306,6 +2424,11 @@ codesandbox-import-utils@^1.2.3: istextorbinary "^2.2.1" lz-string "^1.4.4" +collapse-white-space@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.4.tgz#ce05cf49e54c3277ae573036a26851ba430a0091" + integrity sha512-YfQ1tAUZm561vpYD+5eyWN8+UsceQbSrqqlc/6zDY2gtAE+uZLSdkkovhnGpmCThsvKBFakq4EdY/FF93E8XIw== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -2562,6 +2685,16 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cosmiconfig@^5.0.0: + version "5.0.7" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" + integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^4.0.0" + create-ecdh@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" @@ -2700,6 +2833,11 @@ cssesc@^0.1.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q= +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797" @@ -2712,6 +2850,13 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" @@ -3029,10 +3174,10 @@ debug@^3.1.0, debug@^3.2.5: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" - integrity sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg== +debug@^4.0.0, debug@^4.0.1, debug@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" @@ -3043,7 +3188,15 @@ debug@~3.1.0: dependencies: ms "2.0.0" -decamelize@^1.1.1, decamelize@^1.2.0: +decamelize-keys@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -3247,6 +3400,13 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dir-glob@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" + integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== + dependencies: + path-type "^3.0.0" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -3423,10 +3583,10 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== -electron-to-chromium@^1.3.96: - version "1.3.100" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.100.tgz#899fb088def210aee6b838a47655bbb299190e13" - integrity sha512-cEUzis2g/RatrVf8x26L8lK5VEls1AGnLHk6msluBUg/NTB4wcXzExTsGscFq+Vs4WBBU2zbLLySvD4C0C3hwg== +electron-to-chromium@^1.3.103: + version "1.3.113" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9" + integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g== elliptic@^6.0.0: version "6.4.0" @@ -3441,7 +3601,7 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" -emoji-regex@^7.0.3: +emoji-regex@^7.0.1, emoji-regex@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== @@ -3929,6 +4089,13 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execall@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execall/-/execall-1.0.0.tgz#73d0904e395b3cab0658b08d09ec25307f29bb73" + integrity sha1-c9CQTjlbPKsGWLCNCewlMH8pu3M= + dependencies: + clone-regexp "^1.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -4117,6 +4284,18 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-glob@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.6.tgz#a5d5b697ec8deda468d85a74035290a025a95295" + integrity sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w== + dependencies: + "@mrmlnc/readdir-enhanced" "^2.2.1" + "@nodelib/fs.stat" "^1.1.2" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.3" + micromatch "^3.1.10" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -4180,6 +4359,13 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" +file-entry-cache@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-4.0.0.tgz#633567d15364aefe0b299e1e217735e8f3a9f6e8" + integrity sha512-AVSwsnbV8vH/UVbvgEhf3saVQXORNv0ZzSkvkhQIaia5Tia+JhGTaa/ePUSVoPHQyGayQNmYfkzFi3WZV5zcpA== + dependencies: + flat-cache "^2.0.1" + file-loader@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" @@ -4317,6 +4503,20 @@ flat-cache@^1.2.1: graceful-fs "^4.1.2" write "^0.2.1" +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" + integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== + flush-write-stream@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" @@ -4531,7 +4731,12 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -"glob@5 - 7", glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= + +"glob@5 - 7", glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -4575,6 +4780,13 @@ global-modules@^1.0.0: is-windows "^1.0.1" resolve-dir "^1.0.0" +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + global-prefix@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" @@ -4586,6 +4798,15 @@ global-prefix@^1.0.1: is-windows "^1.0.1" which "^1.2.14" +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + globals@^11.1.0, globals@^11.7.0: version "11.7.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" @@ -4619,6 +4840,31 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-9.0.0.tgz#3800df736dc711266df39b4ce33fe0d481f94c23" + integrity sha512-q0qiO/p1w/yJ0hk8V9x1UXlgsXUxlGd0AHUOXZVXBO6aznDtpx7M8D1kBrCAItoPm+4l8r6ATXV1JpjY2SBQOw== + dependencies: + array-union "^1.0.2" + dir-glob "^2.2.1" + fast-glob "^2.2.6" + glob "^7.1.3" + ignore "^4.0.3" + pify "^4.0.1" + slash "^2.0.0" + +globjoin@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" + integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM= + +gonzales-pe@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.3.tgz#41091703625433285e0aee3aa47829fc1fbeb6f2" + integrity sha512-Kjhohco0esHQnOiqqdJeNz/5fyPkOMD/d6XVjwTAoPGUFh0mCollPUTUTa2OZy4dYNAqlPIQdTiNzJTWdd9Htw== + dependencies: + minimist "1.1.x" + good-listener@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" @@ -4922,6 +5168,11 @@ html-entities@^1.2.0: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2" integrity sha1-QZSMr4XOgv7Tbk5qDtNxpmZDeeI= +html-tags@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" + integrity sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos= + htmlparser2@^3.10.0, htmlparser2@^3.9.0: version "3.10.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464" @@ -5032,11 +5283,16 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^4.0.6: +ignore@^4.0.3, ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.0.4: + version "5.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.0.5.tgz#c663c548d6ce186fb33616a8ccb5d46e56bdbbf9" + integrity sha512-kOC8IUb8HSDMVcYrDVezCxpJkzSQWTAzf3olpKM6o9rM5zpojx23O0Fl8Wr4+qJ6ZbPEHqf1fdwev/DS7v7pmA== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -5047,11 +5303,24 @@ immutable-tuple@^0.4.9: resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.9.tgz#473ebdd6c169c461913a454bf87ef8f601a20ff0" integrity sha512-LWbJPZnidF8eczu7XmcnLBsumuyRBkpwIRPCZxlojouhBo5jEBO4toj6n7hMy6IxHU/c+MqDSWkvaTpPlMQcyA== +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= +import-lazy@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc" + integrity sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ== + import-local@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" @@ -5081,6 +5350,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -5109,7 +5383,7 @@ inherits@2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= -ini@^1.3.4, ini@~1.3.0: +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== @@ -5219,6 +5493,24 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-alphabetical@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.2.tgz#1fa6e49213cb7885b75d15862fb3f3d96c884f41" + integrity sha512-V0xN4BYezDHcBSKb1QHUFMlR4as/XEuCZBzMJUU4n7+Cbt33SmUnSol+pnXFvLxSHNq2CemUXNdaXV6Flg7+xg== + +is-alphanumeric@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4" + integrity sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ= + +is-alphanumerical@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz#1138e9ae5040158dc6ff76b820acd6b7a181fd40" + integrity sha512-pyfU/0kHdISIgslFfZN9nfY1Gk3MquQgUm1mJTjdkEPpkAKNWuBTSqFwewOpR7N351VkErCiyV71zX7mlQQqsg== + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -5236,12 +5528,10 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74= - dependencies: - builtin-modules "^1.0.0" +is-buffer@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" + integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.4" @@ -5274,6 +5564,11 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= +is-decimal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.2.tgz#894662d6a8709d307f3a276ca4339c8fa5dff0ff" + integrity sha512-TRzl7mOCchnhchN+f3ICUCzYvL9ul7R+TYOsZ8xia++knyZAJfv/uA1FvQXsAnYIl1T3B2X5E/J7Wb1QXiIBXg== + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -5292,6 +5587,11 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" @@ -5371,6 +5671,11 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835" + integrity sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A== + is-installed-globally@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" @@ -5444,7 +5749,7 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-obj@^1.0.0: +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= @@ -5503,6 +5808,11 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-supported-regexp-flag@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.1.tgz#21ee16518d2c1dd3edd3e9a0d57e50207ac364ca" + integrity sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ== + is-symbol@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" @@ -5520,11 +5830,21 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= +is-whitespace-character@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed" + integrity sha512-SzM+T5GKUCtLhlHFKt2SDAX2RFzfS6joT91F2/WSi9LxgFdsnhfPK/UIA+JhRR2xuyLdrCys2PiFDrtn1fU5hQ== + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-word-character@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.2.tgz#46a5dac3f2a1840898b91e576cd40d493f3ae553" + integrity sha512-T3FlsX8rCHAH8e7RE7PfOPZVFQlcV3XRF9eOOBQ1uf70OxO7CjjSOjeImMPCADBdYWcStAbVbYvJ1m2D3tb+EA== + is-wsl@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" @@ -6147,10 +6467,10 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" - integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== +js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: + version "3.12.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" + integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -6454,6 +6774,11 @@ kleur@^2.0.1: resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== +known-css-properties@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.11.0.tgz#0da784f115ea77c76b81536d7052e90ee6c86a8a" + integrity sha512-bEZlJzXo5V/ApNNa5z375mJC6Nrz4vG43UgcSCrg2OHC+yuB6j0iDSrY7RQ/+PRofFB03wNIIt9iXIVLr4wc7w== + latest-version@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" @@ -6645,7 +6970,7 @@ lodash@4.x, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lo resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== -log-symbols@^2.1.0: +log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== @@ -6668,6 +6993,11 @@ loglevel@^1.4.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd" integrity sha1-lbOD+Ro8J1b9SrCTZn5DCRYfK80= +longest-streak@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.2.tgz#2421b6ba939a443bb9ffebf596585a50b4c38e2e" + integrity sha512-TmYTeEYxiAmSVdpbnQDXGtvYOIRsCMg89CVZzwzc2o7GFL1CjoiRPjH5ec0NFAVlAx3fVof9dX/t6KKRAo2OWA== + loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -6675,6 +7005,14 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + lowercase-keys@1.0.0, lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -6739,6 +7077,16 @@ map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= + map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -6746,6 +7094,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-escapes@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.2.tgz#e639cbde7b99c841c0bacc8a07982873b46d2122" + integrity sha512-lbRZ2mE3Q9RtLjxZBZ9+IMl68DKIXaVAhwvwn9pmjnPLS0h/6kyBMgNhqi1xFJ/2yv6cSyv0jbiZavZv93JkkA== + markdown-it@^8.4.2: version "8.4.2" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" @@ -6757,6 +7110,11 @@ markdown-it@^8.4.2: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-table@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.2.tgz#c78db948fa879903a41bce522e3b96f801c63786" + integrity sha512-NcWuJFHDA8V3wkDgR/j4+gZx+YQwstPgfQDV8ndUeWWzta3dnDTBxpVzqS9lkmJAuV5YX35lmyojl6HO5JXAgw== + marked@^0.3.12, marked@~0.3.6: version "0.3.19" resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" @@ -6767,6 +7125,11 @@ math-random@^1.0.1: resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" integrity sha1-izqsWIuKZuSXXjzepn97sylgH6w= +mathml-tag-names@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.0.tgz#490b70e062ee24636536e3d9481e333733d00f2c" + integrity sha512-3Zs9P/0zzwTob2pdgT0CHZuMbnSUSp8MB1bddfm+HDmnFWHGT4jvEZRf+2RuPoa+cjdn/z25SEt5gFTqdhvJAg== + md5.js@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" @@ -6775,6 +7138,13 @@ md5.js@^1.3.4: hash-base "^3.0.0" inherits "^2.0.1" +mdast-util-compact@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.2.tgz#c12ebe16fffc84573d3e19767726de226e95f649" + integrity sha512-d2WS98JSDVbpSsBfVvD9TaDMlqPRz7ohM/11G0rp5jOBb5q96RJ6YLszQ/09AAixyzh23FeIpCGqfaamEADtWg== + dependencies: + unist-util-visit "^1.1.0" + mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -6814,6 +7184,21 @@ memory-fs@^0.4.0, memory-fs@~0.4.1: errno "^0.1.3" readable-stream "^2.0.1" +meow@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" + integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + yargs-parser "^10.0.0" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -6833,6 +7218,11 @@ merge-stream@^1.0.1: dependencies: readable-stream "^2.0.1" +merge2@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" + integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== + merge@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" @@ -6876,7 +7266,7 @@ micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.0.4, micromatch@^3.1.4, micromatch@^3.1.6, micromatch@^3.1.8, micromatch@^3.1.9: +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.6, micromatch@^3.1.8, micromatch@^3.1.9: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -6952,11 +7342,24 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" +minimist-options@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" + integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= +minimist@1.1.x: + version "1.1.3" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" + integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag= + minimist@1.2.0, minimist@^1.1.1, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -7248,13 +7651,13 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.3.2: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw== +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== dependencies: hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" + resolve "^1.10.0" semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" @@ -7270,6 +7673,16 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-selector@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03" + integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM= + normalize-url@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" @@ -7314,6 +7727,11 @@ null-check@^1.0.0: resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0= +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -7651,6 +8069,18 @@ parse-asn1@^5.0.0: evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" +parse-entities@^1.0.2, parse-entities@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.0.tgz#9deac087661b2e36814153cb78d7e54a4c5fd6f4" + integrity sha512-XXtDdOPLSB0sHecbEapQi6/58U/ODj/KWfIXmmMCJF/eRn8laX6LZbOyioMoETOOJoWRW8/qTSl5VQkUIfKM5g== + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -7752,7 +8182,7 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= -path-parse@^1.0.5: +path-parse@^1.0.5, path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -7811,6 +8241,11 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= +pify@^4.0.0, pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + pikaday@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/pikaday/-/pikaday-1.6.1.tgz#b91bcb9b8539cedd8d6d08e4e7465e12095671b0" @@ -7897,6 +8332,40 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postcss-html@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.36.0.tgz#b40913f94eaacc2453fd30a1327ad6ee1f88b204" + integrity sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw== + dependencies: + htmlparser2 "^3.10.0" + +postcss-jsx@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/postcss-jsx/-/postcss-jsx-0.36.0.tgz#b7685ed3d070a175ef0aa48f83d9015bd772c82d" + integrity sha512-/lWOSXSX5jlITCKFkuYU2WLFdrncZmjSVyNpHAunEgirZXLwI8RjU556e3Uz4mv0WVHnJA9d3JWb36lK9Yx99g== + dependencies: + "@babel/core" ">=7.1.0" + +postcss-less@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.1.2.tgz#fb67e7ba351dbdf69de3c52eebd1184c52bfaea6" + integrity sha512-66ZBVo1JGkQ7r13M97xcHcyarWpgg21RaqIZWZXHE3XOtb5+ywK1uZWeY1DYkYRkIX/l8Hvxnx9iSKB68nFr+w== + dependencies: + postcss "^7.0.14" + +postcss-markdown@^0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.36.0.tgz#7f22849ae0e3db18820b7b0d5e7833f13a447560" + integrity sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ== + dependencies: + remark "^10.0.1" + unist-util-find-all-after "^1.0.2" + +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ= + postcss-modules-extract-imports@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a" @@ -7928,7 +8397,44 @@ postcss-modules-values@^1.3.0: icss-replace-symbols "^1.1.0" postcss "^6.0.1" -postcss-selector-parser@^3.1.1: +postcss-reporter@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" + integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== + dependencies: + chalk "^2.4.1" + lodash "^4.17.11" + log-symbols "^2.2.0" + postcss "^7.0.7" + +postcss-resolve-nested-selector@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" + integrity sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4= + +postcss-safe-parser@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz#8756d9e4c36fdce2c72b091bbc8ca176ab1fcdea" + integrity sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ== + dependencies: + postcss "^7.0.0" + +postcss-sass@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.3.5.tgz#6d3e39f101a53d2efa091f953493116d32beb68c" + integrity sha512-B5z2Kob4xBxFjcufFnhQ2HqJQ2y/Zs/ic5EZbCywCkxKd756Q40cIQ/veRDwSrw1BF6+4wUgmpm0sBASqVi65A== + dependencies: + gonzales-pe "^4.2.3" + postcss "^7.0.1" + +postcss-scss@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-2.0.0.tgz#248b0a28af77ea7b32b1011aba0f738bda27dea1" + integrity sha512-um9zdGKaDZirMm+kZFKKVsnKPF7zF7qBAtIfTSnZXD1jZ0JNZIxdB6TxQOjCnlSzLRInVl2v3YdBh/M881C4ug== + dependencies: + postcss "^7.0.0" + +postcss-selector-parser@^3.1.0, postcss-selector-parser@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU= @@ -7937,10 +8443,24 @@ postcss-selector-parser@^3.1.1: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-value-parser@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" - integrity sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU= +postcss-selector-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-syntax@^0.36.2: + version "0.36.2" + resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.36.2.tgz#f08578c7d95834574e5593a82dfbfa8afae3b51c" + integrity sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w== + +postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20, postcss@^6.0.23: version "6.0.23" @@ -7951,6 +8471,15 @@ postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20, postcss@^6.0.23: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.7: + version "7.0.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" + integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -8273,6 +8802,11 @@ querystringify@^2.0.0: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== +quick-lru@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" + integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= + randomatic@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" @@ -8358,6 +8892,14 @@ read-pkg-up@^2.0.0: find-up "^2.0.0" read-pkg "^2.0.0" +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + read-pkg-up@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" @@ -8444,6 +8986,14 @@ realpath-native@^1.0.0: dependencies: util.promisify "^1.0.0" +redent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" + integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= + dependencies: + indent-string "^3.0.0" + strip-indent "^2.0.0" + regenerate-unicode-properties@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" @@ -8562,6 +9112,56 @@ regjsparser@^0.3.0: dependencies: jsesc "~0.5.0" +remark-parse@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-6.0.3.tgz#c99131052809da482108413f87b0ee7f52180a3a" + integrity sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg== + dependencies: + collapse-white-space "^1.0.2" + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + is-word-character "^1.0.0" + markdown-escapes "^1.0.0" + parse-entities "^1.1.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + trim "0.0.1" + trim-trailing-lines "^1.0.0" + unherit "^1.0.4" + unist-util-remove-position "^1.0.0" + vfile-location "^2.0.0" + xtend "^4.0.1" + +remark-stringify@^6.0.0: + version "6.0.4" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-6.0.4.tgz#16ac229d4d1593249018663c7bddf28aafc4e088" + integrity sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg== + dependencies: + ccount "^1.0.0" + is-alphanumeric "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + longest-streak "^2.0.1" + markdown-escapes "^1.0.0" + markdown-table "^1.1.0" + mdast-util-compact "^1.0.0" + parse-entities "^1.0.2" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + stringify-entities "^1.0.1" + unherit "^1.0.4" + xtend "^4.0.1" + +remark@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/remark/-/remark-10.0.1.tgz#3058076dc41781bf505d8978c291485fe47667df" + integrity sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ== + dependencies: + remark-parse "^6.0.0" + remark-stringify "^6.0.0" + unified "^7.0.0" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -8577,7 +9177,7 @@ repeat-string@^0.2.2: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" integrity sha1-x6jTI2BoNiBZp+RlH8aITosftK4= -repeat-string@^1.5.2, repeat-string@^1.6.1: +repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -8589,6 +9189,11 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +replace-ext@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= + request-promise-core@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" @@ -8686,6 +9291,11 @@ resolve-from@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" integrity sha1-six699nWiBvItuZTM17rywoYh0g= +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -8696,12 +9306,12 @@ resolve@1.1.7, resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" - integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== +resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" + integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== dependencies: - path-parse "^1.0.5" + path-parse "^1.0.6" responselike@1.0.2: version "1.0.2" @@ -8728,12 +9338,12 @@ rfdc@^1.1.2: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.2.tgz#e6e72d74f5dc39de8f538f65e00c36c18018e349" integrity sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA== -rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== +rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== dependencies: - glob "^7.0.5" + glob "^7.1.3" ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.1" @@ -9052,10 +9662,15 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= -slice-ansi@2.0.0: +slash@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.0.0.tgz#5373bdb8559b45676e8541c66916cdd6251612e7" - integrity sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ== + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== dependencies: ansi-styles "^3.2.0" astral-regex "^1.0.0" @@ -9281,6 +9896,11 @@ spdy@^4.0.0: select-hose "^2.0.0" spdy-transport "^3.0.0" +specificity@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" + integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -9333,6 +9953,11 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== +state-toggle@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.1.tgz#c3cb0974f40a6a0f8e905b96789eb41afa1cde3a" + integrity sha512-Qe8QntFrrpWTnHwvwj2FZTgv+PKIsp0B9VxLzLLbSpPXWOgRgc5LVj/aTiSfK1RqIeF9jeC1UeOH8Q8y60A7og== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -9433,6 +10058,15 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1" + integrity sha512-rr8CUxBbvOZDUvc5lNIJ+OC1nPVpz+Siw9VBtUjB9b6jZehZLFt0JMCZzShFHIsI8cbhm0EsNIfWJMFV3cu3Ew== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.0.0" + string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -9445,6 +10079,16 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= +stringify-entities@^1.0.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-1.3.2.tgz#a98417e5471fd227b3e45d3db1861c11caf668f7" + integrity sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A== + dependencies: + character-entities-html4 "^1.0.0" + character-entities-legacy "^1.0.0" + is-alphanumerical "^1.0.0" + is-hexadecimal "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -9459,6 +10103,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" + integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== + dependencies: + ansi-regex "^4.0.0" + strip-bom@3.0.0, strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -9483,6 +10134,11 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-indent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= + strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -9496,6 +10152,87 @@ style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" +style-search@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" + integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI= + +stylelint-config-recommended@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-2.1.0.tgz#f526d5c771c6811186d9eaedbed02195fee30858" + integrity sha512-ajMbivOD7JxdsnlS5945KYhvt7L/HwN6YeYF2BH6kE4UCLJR0YvXMf+2j7nQpJyYLZx9uZzU5G1ZOSBiWAc6yA== + +stylelint-scss@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.5.3.tgz#e158b3061eeec26d7f6088f346998a797432f3c8" + integrity sha512-QESQUOY1ldU5tlJTTM3Megz/QtJ39S58ByjZ7dZobGDq9qMjy5jbC7PDUasrv/T7pB1UbpPojpxX9K1OR7IPEg== + dependencies: + lodash "^4.17.11" + postcss-media-query-parser "^0.2.3" + postcss-resolve-nested-selector "^0.1.1" + postcss-selector-parser "^5.0.0" + postcss-value-parser "^3.3.1" + +stylelint@^9.10.1: + version "9.10.1" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.10.1.tgz#5f0ee3701461dff1d68284e1386efe8f0677a75d" + integrity sha512-9UiHxZhOAHEgeQ7oLGwrwoDR8vclBKlSX7r4fH0iuu0SfPwFaLkb1c7Q2j1cqg9P7IDXeAV2TvQML/fRQzGBBQ== + dependencies: + autoprefixer "^9.0.0" + balanced-match "^1.0.0" + chalk "^2.4.1" + cosmiconfig "^5.0.0" + debug "^4.0.0" + execall "^1.0.0" + file-entry-cache "^4.0.0" + get-stdin "^6.0.0" + global-modules "^2.0.0" + globby "^9.0.0" + globjoin "^0.1.4" + html-tags "^2.0.0" + ignore "^5.0.4" + import-lazy "^3.1.0" + imurmurhash "^0.1.4" + known-css-properties "^0.11.0" + leven "^2.1.0" + lodash "^4.17.4" + log-symbols "^2.0.0" + mathml-tag-names "^2.0.1" + meow "^5.0.0" + micromatch "^3.1.10" + normalize-selector "^0.2.0" + pify "^4.0.0" + postcss "^7.0.13" + postcss-html "^0.36.0" + postcss-jsx "^0.36.0" + postcss-less "^3.1.0" + postcss-markdown "^0.36.0" + postcss-media-query-parser "^0.2.3" + postcss-reporter "^6.0.0" + postcss-resolve-nested-selector "^0.1.1" + postcss-safe-parser "^4.0.0" + postcss-sass "^0.3.5" + postcss-scss "^2.0.0" + postcss-selector-parser "^3.1.0" + postcss-syntax "^0.36.2" + postcss-value-parser "^3.3.0" + resolve-from "^4.0.0" + signal-exit "^3.0.2" + slash "^2.0.0" + specificity "^0.4.1" + string-width "^3.0.0" + style-search "^0.1.0" + sugarss "^2.0.0" + svg-tags "^1.0.0" + table "^5.0.0" + +sugarss@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d" + integrity sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ== + dependencies: + postcss "^7.0.2" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -9515,6 +10252,18 @@ supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-co dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +svg-tags@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" + integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= + svg4everybody@2.1.9: version "2.1.9" resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d" @@ -9530,15 +10279,15 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= -table@^5.0.2: - version "5.1.1" - resolved "https://registry.yarnpkg.com/table/-/table-5.1.1.tgz#92030192f1b7b51b6eeab23ed416862e47b70837" - integrity sha512-NUjapYb/qd4PeFW03HnAuOJ7OMcBkJlqeClWxeNlQ0lXGSb52oZXGzkO0/I0ARegQ2eUT1g2VDJH0eUxDRcHmw== +table@^5.0.0, table@^5.0.2: + version "5.2.3" + resolved "https://registry.yarnpkg.com/table/-/table-5.2.3.tgz#cde0cc6eb06751c009efab27e8c820ca5b67b7f2" + integrity sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ== dependencies: - ajv "^6.6.1" + ajv "^6.9.1" lodash "^4.17.11" - slice-ansi "2.0.0" - string-width "^2.1.1" + slice-ansi "^2.1.0" + string-width "^3.0.0" taffydb@2.6.2: version "2.6.2" @@ -9831,11 +10580,31 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +trim-newlines@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +trim-trailing-lines@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz#e0ec0810fd3c3f1730516b45f49083caaf2774d9" + integrity sha512-bWLv9BbWbbd7mlqqs2oQYnLD/U/ZqeJeJwbO0FG2zA1aTq+HTvxfHNKFa/HGCVyJpDiioUYaBhfiT6rgk+l4mg== + +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0= + +trough@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.3.tgz#e29bd1614c6458d44869fc28b255ab7857ef7c24" + integrity sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw== + tryer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7" @@ -9945,6 +10714,14 @@ underscore@~1.8.3: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= +unherit@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.1.tgz#132748da3e88eab767e08fabfbb89c5e9d28628c" + integrity sha512-+XZuV691Cn4zHsK0vkKYwBEwB74T3IZIcxrgn2E4rKwTfFyI1zCh7X7grwh9Re08fdPlarIdyWgI8aVB3F5A5g== + dependencies: + inherits "^2.0.1" + xtend "^4.0.1" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -9968,6 +10745,20 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== +unified@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/unified/-/unified-7.1.0.tgz#5032f1c1ee3364bd09da12e27fdd4a7553c7be13" + integrity sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw== + dependencies: + "@types/unist" "^2.0.0" + "@types/vfile" "^3.0.0" + bail "^1.0.0" + extend "^3.0.0" + is-plain-obj "^1.1.0" + trough "^1.0.0" + vfile "^3.0.0" + x-is-string "^0.1.0" + union-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" @@ -10004,6 +10795,44 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +unist-util-find-all-after@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-1.0.2.tgz#9be49cfbae5ca1566b27536670a92836bf2f8d6d" + integrity sha512-nDl79mKpffXojLpCimVXnxhlH/jjaTnDuScznU9J4jjsaUtBdDbxmlc109XtcqxY4SDO0SwzngsxxW8DIISt1w== + dependencies: + unist-util-is "^2.0.0" + +unist-util-is@^2.0.0, unist-util-is@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.2.tgz#1193fa8f2bfbbb82150633f3a8d2eb9a1c1d55db" + integrity sha512-YkXBK/H9raAmG7KXck+UUpnKiNmUdB+aBGrknfQ4EreE1banuzrKABx3jP6Z5Z3fMSPMQQmeXBlKpCbMwBkxVw== + +unist-util-remove-position@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz#86b5dad104d0bbfbeb1db5f5c92f3570575c12cb" + integrity sha512-XxoNOBvq1WXRKXxgnSYbtCF76TJrRoe5++pD4cCBsssSiWSnPEktyFrFLE8LTk3JW5mt9hB0Sk5zn4x/JeWY7Q== + dependencies: + unist-util-visit "^1.1.0" + +unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6" + integrity sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ== + +unist-util-visit-parents@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.0.1.tgz#63fffc8929027bee04bfef7d2cce474f71cb6217" + integrity sha512-6B0UTiMfdWql4cQ03gDTCSns+64Zkfo2OCbK31Ov0uMizEz+CJeAp0cgZVb5Fhmcd7Bct2iRNywejT0orpbqUA== + dependencies: + unist-util-is "^2.1.2" + +unist-util-visit@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.0.tgz#1cb763647186dc26f5e1df5db6bd1e48b3cc2fb1" + integrity sha512-FiGu34ziNsZA3ZUteZxSFaczIjGmksfSgdKqBfOejrrfzyUy5b7YrlzT1Bcvi+djkYDituJDy2XB7tGTeBieKw== + dependencies: + unist-util-visit-parents "^2.0.0" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -10178,6 +11007,28 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-location@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.4.tgz#2a5e7297dd0d9e2da4381464d04acc6b834d3e55" + integrity sha512-KRL5uXQPoUKu+NGvQVL4XLORw45W62v4U4gxJ3vRlDfI9QsT4ZN1PNXn/zQpKUulqGDpYuT0XDfp5q9O87/y/w== + +vfile-message@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.1.1.tgz#5833ae078a1dfa2d96e9647886cd32993ab313e1" + integrity sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA== + dependencies: + unist-util-stringify-position "^1.1.1" + +vfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-3.0.1.tgz#47331d2abe3282424f4a4bb6acd20a44c4121803" + integrity sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ== + dependencies: + is-buffer "^2.0.0" + replace-ext "1.0.0" + unist-util-stringify-position "^1.0.0" + vfile-message "^1.0.0" + visibilityjs@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63" @@ -10529,7 +11380,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0: +which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -10598,6 +11449,13 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.1.0: imurmurhash "^0.1.4" signal-exit "^3.0.2" +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + write@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" @@ -10628,6 +11486,11 @@ ws@~3.3.1: safe-buffer "~5.1.0" ultron "~1.1.0" +x-is-string@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82" + integrity sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI= + xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" @@ -10698,7 +11561,7 @@ yallist@^3.0.0, yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" integrity sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k= -yargs-parser@^10.1.0: +yargs-parser@^10.0.0, yargs-parser@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== |