summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzegorz@gitlab.com>2018-04-25 13:40:13 +0000
committerGrzegorz Bizon <grzegorz@gitlab.com>2018-04-25 13:40:13 +0000
commit37b9a441c1b74c679419ecd6c9560ab4390898b9 (patch)
tree2fce001235b0ea3adb06bf1d37a6c21d5d336b6e
parentdf6a2d423ea4dac3484669f6f49c535b36288d5d (diff)
parent5beb4ddebfa23b288dc088c7175c2456d411f4f1 (diff)
downloadgitlab-ce-37b9a441c1b74c679419ecd6c9560ab4390898b9.tar.gz
Merge branch 'master' into 'backstage/gb/migrate-pipeline-stages-index'
# Conflicts: # db/schema.rb
-rw-r--r--.babelrc6
-rw-r--r--Gemfile.lock9
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue245
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue113
-rw-r--r--app/assets/javascripts/ide/components/ide.vue114
-rw-r--r--app/assets/javascripts/ide/constants.js5
-rw-r--r--app/assets/javascripts/ide/lib/editor.js33
-rw-r--r--app/assets/javascripts/ide/lib/keymap.json11
-rw-r--r--app/assets/javascripts/ide/stores/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/getters.js16
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js5
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js1
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js1
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue2
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js3
-rw-r--r--app/assets/javascripts/notes/stores/getters.js3
-rw-r--r--app/assets/javascripts/registry/stores/actions.js3
-rw-r--r--app/assets/javascripts/registry/stores/getters.js3
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss30
-rw-r--r--app/assets/stylesheets/framework/mobile.scss4
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss21
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb3
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb31
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb144
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/project.rb16
-rw-r--r--app/models/project_ci_cd_setting.rb16
-rw-r--r--app/models/protected_branch.rb9
-rw-r--r--app/presenters/project_presenter.rb17
-rw-r--r--app/services/quick_actions/interpret_service.rb26
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml14
-rwxr-xr-x[-rw-r--r--]app/views/projects/forks/new.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml2
-rw-r--r--app/views/shared/_group_form.html.haml7
-rw-r--r--changelogs/unreleased/10244-add-project-ci-cd-settings.yml5
-rw-r--r--changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml5
-rw-r--r--changelogs/unreleased/45398-fix-rss-button.yml5
-rw-r--r--changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml5
-rwxr-xr-xchangelogs/unreleased/accessible-text.yml6
-rw-r--r--changelogs/unreleased/add-copy-metadata-command.yml5
-rw-r--r--changelogs/unreleased/align-project-avatar-on-small-viewports.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-maintainer-push-error.yml5
-rw-r--r--changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml5
-rw-r--r--changelogs/unreleased/ide-file-finder.yml5
-rw-r--r--changelogs/unreleased/restore-label-underline-color.yml5
-rw-r--r--changelogs/unreleased/sh-bump-lograge.yml5
-rw-r--r--changelogs/unreleased/show-group-id-in-group-settings.yml5
-rw-r--r--changelogs/unreleased/show-runners-description-on-jobs-page.yml5
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/initializers/2_gitlab.rb1
-rw-r--r--config/initializers/deprecations.rb2
-rw-r--r--config/initializers/lograge.rb18
-rw-r--r--config/karma.config.js2
-rw-r--r--config/routes/user.rb18
-rw-r--r--config/settings.rb (renamed from lib/settings.rb)0
-rw-r--r--db/migrate/20180403035759_create_project_ci_cd_settings.rb68
-rw-r--r--db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb27
-rw-r--r--db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb34
-rw-r--r--db/schema.rb10
-rw-r--r--doc/administration/high_availability/nfs.md94
-rw-r--r--doc/ci/runners/README.md22
-rw-r--r--doc/development/testing_guide/frontend_testing.md48
-rw-r--r--doc/raketasks/backup_restore.md7
-rw-r--r--doc/security/img/outbound_requests_section.pngbin0 -> 18064 bytes
-rw-r--r--doc/security/webhooks.md13
-rw-r--r--doc/user/project/quick_actions.md7
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md17
-rw-r--r--ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb22
-rw-r--r--ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb29
-rw-r--r--features/project/find_file.feature42
-rw-r--r--features/steps/project/project_find_file.rb72
-rw-r--r--features/steps/shared/paths.rb4
-rw-r--r--lib/backup/files.rb2
-rw-r--r--lib/backup/helper.rb14
-rw-r--r--lib/backup/repository.rb2
-rw-r--r--lib/gitlab.rb3
-rw-r--r--lib/gitlab/auth/ldap/user.rb9
-rw-r--r--lib/gitlab/auth/o_auth/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/o_auth/user.rb14
-rw-r--r--lib/gitlab/auth/omniauth_identity_linker_base.rb47
-rw-r--r--lib/gitlab/auth/saml/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/saml/user.rb13
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/import_export/relation_factory.rb3
-rw-r--r--lib/gitlab/user_access.rb8
-rw-r--r--locale/gitlab.pot7
-rw-r--r--package.json8
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb58
-rw-r--r--spec/factories/projects.rb7
-rw-r--r--spec/fast_spec_helper.rb3
-rw-r--r--spec/features/oauth_login_spec.rb49
-rw-r--r--spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb52
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb66
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin341299 -> 343091 bytes
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb43
-rw-r--r--spec/fixtures/exported-project.gzbin2306 -> 2560 bytes
-rw-r--r--spec/javascripts/.eslintrc1
-rw-r--r--spec/javascripts/activities_spec.js75
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js2
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js2
-rw-r--r--spec/javascripts/comment_type_toggle_spec.js5
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/commits_spec.js12
-rw-r--r--spec/javascripts/droplab/hook_spec.js5
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js17
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js6
-rw-r--r--spec/javascripts/gl_dropdown_spec.js7
-rw-r--r--spec/javascripts/groups/components/app_spec.js5
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js5
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js2
-rw-r--r--spec/javascripts/ide/components/file_finder/index_spec.js308
-rw-r--r--spec/javascripts/ide/components/file_finder/item_spec.js140
-rw-r--r--spec/javascripts/ide/components/ide_spec.js65
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js24
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js20
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js8
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js8
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js25
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js15
-rw-r--r--spec/javascripts/job_spec.js3
-rw-r--r--spec/javascripts/jobs/sidebar_details_block_spec.js2
-rw-r--r--spec/javascripts/lib/utils/csrf_token_spec.js2
-rw-r--r--spec/javascripts/lib/utils/image_utility_spec.js8
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js13
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js7
-rw-r--r--spec/javascripts/pager_spec.js43
-rw-r--r--spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js5
-rw-r--r--spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js5
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js2
-rw-r--r--spec/javascripts/right_sidebar_spec.js4
-rw-r--r--spec/javascripts/search_autocomplete_spec.js4
-rw-r--r--spec/javascripts/shortcuts_dashboard_navigation_spec.js9
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js7
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js2
-rw-r--r--spec/javascripts/test_bundle.js14
-rw-r--r--spec/javascripts/todos_spec.js5
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js2
-rw-r--r--spec/javascripts/u2f/register_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js17
-rw-r--r--spec/lib/backup/files_spec.rb14
-rw-r--r--spec/lib/backup/repository_spec.rb12
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb8
-rw-r--r--spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb62
-rw-r--r--spec/lib/gitlab/auth/saml/identity_linker_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/gitlab/user_access_spec.rb12
-rw-r--r--spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb32
-rw-r--r--spec/models/environment_spec.rb4
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb24
-rw-r--r--spec/models/project_spec.rb55
-rw-r--r--spec/presenters/project_presenter_spec.rb11
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb76
-rw-r--r--spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb33
-rw-r--r--spec/support/helpers/ldap_helpers.rb4
-rw-r--r--spec/support/helpers/login_helpers.rb4
-rw-r--r--spec/support/shared_examples/issuables_list_metadata_shared_examples.rb50
-rw-r--r--vendor/project_templates/express.tar.gzbin5614 -> 5608 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25007 -> 25004 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50945 -> 50938 bytes
-rw-r--r--yarn.lock8
180 files changed, 2745 insertions, 666 deletions
diff --git a/.babelrc b/.babelrc
index 8cf07b73420..50d85f58d69 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,6 +1,9 @@
{
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": {
+ "karma": {
+ "plugins": ["rewire"]
+ },
"coverage": {
"plugins": [
[
@@ -14,7 +17,8 @@
{
"process.env.BABEL_ENV": "coverage"
}
- ]
+ ],
+ "rewire"
]
}
}
diff --git a/Gemfile.lock b/Gemfile.lock
index 1ba99df5e44..d5e1c428e25 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -483,10 +483,11 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
- lograge (0.5.1)
- actionpack (>= 4, < 5.2)
- activesupport (>= 4, < 5.2)
- railties (>= 4, < 5.2)
+ lograge (0.10.0)
+ actionpack (>= 4)
+ activesupport (>= 4)
+ railties (>= 4)
+ request_store (~> 1.0)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
new file mode 100644
index 00000000000..ea2b13a8b21
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -0,0 +1,245 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import VirtualList from 'vue-virtual-scroll-list';
+import Item from './item.vue';
+import router from '../../ide_router';
+import {
+ MAX_FILE_FINDER_RESULTS,
+ FILE_FINDER_ROW_HEIGHT,
+ FILE_FINDER_EMPTY_ROW_HEIGHT,
+} from '../../constants';
+import {
+ UP_KEY_CODE,
+ DOWN_KEY_CODE,
+ ENTER_KEY_CODE,
+ ESC_KEY_CODE,
+} from '../../../lib/utils/keycodes';
+
+export default {
+ components: {
+ Item,
+ VirtualList,
+ },
+ data() {
+ return {
+ focusedIndex: 0,
+ searchText: '',
+ mouseOver: false,
+ cancelMouseOver: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['allBlobs']),
+ ...mapState(['fileFindVisible', 'loading']),
+ filteredBlobs() {
+ const searchText = this.searchText.trim();
+
+ if (searchText === '') {
+ return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
+ }
+
+ return fuzzaldrinPlus
+ .filter(this.allBlobs, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ })
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+ },
+ filteredBlobsLength() {
+ return this.filteredBlobs.length;
+ },
+ listShowCount() {
+ return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
+ },
+ listHeight() {
+ return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
+ },
+ showClearInputButton() {
+ return this.searchText.trim() !== '';
+ },
+ },
+ watch: {
+ fileFindVisible() {
+ this.$nextTick(() => {
+ if (!this.fileFindVisible) {
+ this.searchText = '';
+ } else {
+ this.focusedIndex = 0;
+
+ if (this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ }
+ });
+ },
+ searchText() {
+ this.focusedIndex = 0;
+ },
+ focusedIndex() {
+ if (!this.mouseOver) {
+ this.$nextTick(() => {
+ const el = this.$refs.virtualScrollList.$el;
+ const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
+ const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
+
+ if (this.focusedIndex === 0) {
+ // if index is the first index, scroll straight to start
+ el.scrollTop = 0;
+ } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
+ // if index is the last index, scroll to the end
+ el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop >= bottom + el.scrollTop) {
+ // if element is off the bottom of the scroll list, scroll down one item
+ el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop < el.scrollTop) {
+ // if element is off the top of the scroll list, scroll up one item
+ el.scrollTop = scrollTop;
+ }
+ });
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ clearSearchInput() {
+ this.searchText = '';
+
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ },
+ onKeydown(e) {
+ switch (e.keyCode) {
+ case UP_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex > 0) {
+ this.focusedIndex -= 1;
+ } else {
+ this.focusedIndex = this.filteredBlobsLength - 1;
+ }
+ break;
+ case DOWN_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex < this.filteredBlobsLength - 1) {
+ this.focusedIndex += 1;
+ } else {
+ this.focusedIndex = 0;
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ onKeyup(e) {
+ switch (e.keyCode) {
+ case ENTER_KEY_CODE:
+ this.openFile(this.filteredBlobs[this.focusedIndex]);
+ break;
+ case ESC_KEY_CODE:
+ this.toggleFileFinder(false);
+ break;
+ default:
+ break;
+ }
+ },
+ openFile(file) {
+ this.toggleFileFinder(false);
+ router.push(`/project${file.url}`);
+ },
+ onMouseOver(index) {
+ if (!this.cancelMouseOver) {
+ this.mouseOver = true;
+ this.focusedIndex = index;
+ }
+ },
+ onMouseMove(index) {
+ this.cancelMouseOver = false;
+ this.onMouseOver(index);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-finder-overlay"
+ @mousedown.self="toggleFileFinder(false)"
+ >
+ <div
+ class="dropdown-menu diff-file-changes ide-file-finder show"
+ >
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ :placeholder="__('Search files')"
+ autocomplete="off"
+ v-model="searchText"
+ ref="searchInput"
+ @keydown="onKeydown($event)"
+ @keyup="onKeyup($event)"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ :class="{
+ hidden: showClearInputButton
+ }"
+ ></i>
+ <i
+ role="button"
+ :aria-label="__('Clear search input')"
+ class="fa fa-times dropdown-input-clear"
+ :class="{
+ show: showClearInputButton
+ }"
+ @click="clearSearchInput"
+ ></i>
+ </div>
+ <div>
+ <virtual-list
+ :size="listHeight"
+ :remain="listShowCount"
+ wtag="ul"
+ ref="virtualScrollList"
+ >
+ <template v-if="filteredBlobsLength">
+ <li
+ v-for="(file, index) in filteredBlobs"
+ :key="file.key"
+ >
+ <item
+ class="disable-hover"
+ :file="file"
+ :search-text="searchText"
+ :focused="index === focusedIndex"
+ :index="index"
+ @click="openFile"
+ @mouseover="onMouseOver"
+ @mousemove="onMouseMove"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="dropdown-menu-empty-item"
+ >
+ <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
+ <template v-if="loading">
+ {{ __('Loading...') }}
+ </template>
+ <template v-else>
+ {{ __('No files found.') }}
+ </template>
+ </div>
+ </li>
+ </virtual-list>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
new file mode 100644
index 00000000000..d4427420207
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -0,0 +1,113 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import FileIcon from '../../../vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '../changed_file_icon.vue';
+
+const MAX_PATH_LENGTH = 60;
+
+export default {
+ components: {
+ ChangedFileIcon,
+ FileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ focused: {
+ type: Boolean,
+ required: true,
+ },
+ searchText: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ pathWithEllipsis() {
+ const path = this.file.path;
+
+ return path.length < MAX_PATH_LENGTH
+ ? path
+ : `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
+ },
+ nameSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.file.name, this.searchText);
+ },
+ pathSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
+ },
+ },
+ methods: {
+ clickRow() {
+ this.$emit('click', this.file);
+ },
+ mouseOverRow() {
+ this.$emit('mouseover', this.index);
+ },
+ mouseMove() {
+ this.$emit('mousemove', this.index);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="diff-changed-file"
+ :class="{
+ 'is-focused': focused,
+ }"
+ @click.prevent="clickRow"
+ @mouseover="mouseOverRow"
+ @mousemove="mouseMove"
+ >
+ <file-icon
+ :file-name="file.name"
+ :size="16"
+ css-classes="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ class="diff-changed-file-name"
+ >
+ <span
+ v-for="(char, index) in file.name.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </strong>
+ <span
+ class="diff-changed-file-path prepend-top-5"
+ >
+ <span
+ v-for="(char, index) in pathWithEllipsis.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </span>
+ </span>
+ <span
+ v-if="file.changed || file.tempFile"
+ class="diff-changed-stats"
+ >
+ <changed-file-icon
+ :file="file"
+ />
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1c237c0ec97..0274fc7d299 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,55 +1,91 @@
<script>
-import { mapState, mapGetters } from 'vuex';
-import ideSidebar from './ide_side_bar.vue';
-import ideContextbar from './ide_context_bar.vue';
-import repoTabs from './repo_tabs.vue';
-import ideStatusBar from './ide_status_bar.vue';
-import repoEditor from './repo_editor.vue';
+ import { mapActions, mapState, mapGetters } from 'vuex';
+ import Mousetrap from 'mousetrap';
+ import ideSidebar from './ide_side_bar.vue';
+ import ideContextbar from './ide_context_bar.vue';
+ import repoTabs from './repo_tabs.vue';
+ import ideStatusBar from './ide_status_bar.vue';
+ import repoEditor from './repo_editor.vue';
+ import FindFile from './file_finder/index.vue';
-export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- ideStatusBar,
- repoEditor,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
+ const originalStopCallback = Mousetrap.stopCallback;
+
+ export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ ideStatusBar,
+ repoEditor,
+ FindFile,
},
- noChangesStateSvgPath: {
- type: String,
- required: true,
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
- committedStateSvgPath: {
- type: String,
- required: true,
+ computed: {
+ ...mapState([
+ 'changedFiles',
+ 'openFiles',
+ 'viewer',
+ 'currentMergeRequestId',
+ 'fileFindVisible',
+ ]),
+ ...mapGetters(['activeFile', 'hasChanges']),
},
- },
- computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
- ...mapGetters(['activeFile', 'hasChanges']),
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
+
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
- Object.assign(e, {
- returnValue,
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ this.toggleFileFinder(!this.fileFindVisible);
});
- return returnValue;
- };
- },
-};
+
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ mousetrapStopCallback(e, el, combo) {
+ if (combo === 't' && el.classList.contains('dropdown-input-field')) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
+
+ return originalStopCallback(e, el, combo);
+ },
+ },
+ };
</script>
<template>
<div
class="ide-view"
>
+ <find-file
+ v-show="fileFindVisible"
+ />
<ide-sidebar />
<div
class="multi-file-edit-pane"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index b60d042e0be..b06da9f95d1 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -1,3 +1,8 @@
// Fuzzy file finder
+export const MAX_FILE_FINDER_RESULTS = 40;
+export const FILE_FINDER_ROW_HEIGHT = 55;
+export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+
+// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 2d3ee7d4f48..b65d9c68a0b 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,10 +1,12 @@
import _ from 'underscore';
+import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
+import keymap from './keymap.json';
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
@@ -53,6 +55,8 @@ export default class Editor {
)),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
@@ -73,6 +77,8 @@ export default class Editor {
})),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
@@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700;
}
+
+ addCommands() {
+ const getKeyCode = key => {
+ const monacoKeyMod = key.indexOf('KEY_') === 0;
+
+ return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ };
+
+ keymap.forEach(command => {
+ const keybindings = command.bindings.map(binding => {
+ const keys = binding.split('+');
+
+ // eslint-disable-next-line no-bitwise
+ return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
+ });
+
+ this.instance.addAction({
+ id: command.id,
+ label: command.label,
+ keybindings,
+ run() {
+ store.dispatch(command.action.name, command.action.params);
+ return null;
+ },
+ });
+ });
+ }
}
diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json
new file mode 100644
index 00000000000..131abfebbed
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/keymap.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": "file-finder",
+ "label": "File finder",
+ "bindings": ["CtrlCmd+KEY_P"],
+ "action": {
+ "name": "toggleFileFinder",
+ "params": true
+ }
+ }
+]
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index cecb4d215ba..cbe43f5f7f2 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -137,7 +137,13 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const toggleFileFinder = ({ commit }, fileFindVisible) =>
+ commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 8518d2f6f06..ec1ea155aee 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -42,4 +42,20 @@ export const collapseButtonTooltip = state =>
export const hasMergeRequest = state => !!state.currentMergeRequestId;
+export const allBlobs = state =>
+ Object.keys(state.entries)
+ .reduce((acc, key) => {
+ const entry = state.entries[key];
+
+ if (entry.type === 'blob') {
+ acc.push(entry);
+ }
+
+ return acc;
+ }, [])
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index b26512e213a..119debaf5f3 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -185,3 +185,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(types.UPDATE_LOADING, false);
});
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index 9c3905a0b0d..d01060201f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index f5f95b755c8..c7f08449d03 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -58,3 +58,5 @@ export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+
+export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index fbe342f9126..2a6c136aeed 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -100,6 +100,11 @@ export default {
delayViewerUpdated,
});
},
+ [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
+ Object.assign(state, {
+ fileFindVisible,
+ });
+ },
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path);
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index dd7dcba8ac7..c3041c77199 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -4,6 +4,7 @@ export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
+ lastOpenedAt: new Date().getTime(),
});
if (active && !state.entries[path].pending) {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 34975ac3144..3470bb9aec0 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -18,4 +18,5 @@ export default () => ({
entries: {},
viewer: 'editor',
delayViewerUpdated: false,
+ fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 8a222da14c0..86612d845e0 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -42,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit',
previewMode: null,
size: 0,
+ lastOpenedAt: 0,
});
export const decorateData = entity => {
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 4cd44bf7a76..db19dc9b238 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -45,7 +45,7 @@ export default {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
- return `#${this.job.runner.id}`;
+ return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
new file mode 100644
index 00000000000..5e0f9b612a2
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -0,0 +1,4 @@
+export const UP_KEY_CODE = 38;
+export const DOWN_KEY_CODE = 40;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 244a6980b5a..98ce070288e 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
scrollToElement(el);
}
};
+
+// 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 f89591a54d6..787be6f4c99 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 795b39bb3dc..593a43c7cc1 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
index 588f479c492..f4923512578 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -1,2 +1,5 @@
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index d0dda50a835..e058a0b35b7 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -472,6 +472,7 @@ img.emoji {
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
+.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cc5fac6816d..664aade7375 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -43,7 +43,7 @@
border-color: $gray-darkest;
}
- [data-toggle="dropdown"] {
+ [data-toggle='dropdown'] {
outline: 0;
}
}
@@ -172,7 +172,11 @@
color: $brand-danger;
}
- &:hover,
+ &.disable-hover {
+ text-decoration: none;
+ }
+
+ &:not(.disable-hover):hover,
&:active,
&:focus,
&.is-focused {
@@ -508,17 +512,16 @@
}
&.is-indeterminate::before {
- content: "\f068";
+ content: '\f068';
}
&.is-active::before {
- content: "\f00c";
+ content: '\f00c';
}
}
}
}
-
.dropdown-title {
position: relative;
padding: 2px 25px 10px;
@@ -724,7 +727,6 @@
}
}
-
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
@@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-frequent-container,
- .projects-list-search-container, {
+ .projects-list-search-container {
padding: 8px 0;
overflow-y: auto;
+
+ li.section-empty.section-failure {
+ color: $callout-danger-color;
+ }
}
.section-header,
@@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
font-size: $gl-font-size;
}
- .projects-list-frequent-container,
- .projects-list-search-container {
- li.section-empty.section-failure {
- color: $callout-danger-color;
- }
- }
-
.search-input-container {
position: relative;
padding: 4px $gl-padding;
@@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-item-container {
- .project-item-avatar-container
- .project-item-metadata-container {
+ .project-item-avatar-container .project-item-metadata-container {
float: left;
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 8604e753c18..9e03bb98b8e 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -40,10 +40,6 @@
.project-home-panel {
padding-left: 0 !important;
- .project-avatar {
- display: block;
- }
-
.project-repo-buttons,
.git-clone-holder {
display: none;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 17c31d6b184..66dbe403385 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -241,8 +241,6 @@
}
.scrolling-tabs-container {
- position: relative;
-
.merge-request-tabs-container & {
overflow: hidden;
}
@@ -272,8 +270,6 @@
}
.inner-page-scroll-tabs {
- position: relative;
-
.fade-right {
@include fade(left, $white-light);
right: 0;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index b0852adb459..d81236c5883 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -314,6 +314,10 @@
display: inline-flex;
vertical-align: top;
+ &:hover .color-label {
+ text-decoration: underline;
+ }
+
.label {
vertical-align: inherit;
font-size: $label-font-size;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 450ef7d6b7e..6d77260b658 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -17,6 +17,7 @@
}
.ide-view {
+ position: relative;
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 0;
@@ -876,6 +877,26 @@
font-weight: $gl-font-weight-bold;
}
+.ide-file-finder-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 100;
+}
+
+.ide-file-finder {
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+
+ .highlighted {
+ color: $blue-500;
+ font-weight: $gl-font-weight-bold;
+ }
+}
+
.ide-commit-message-field {
height: 200px;
background-color: $white-light;
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 2fdf346ef44..69a053d4246 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
+ # Set @user for Devise views
+ @user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 4114ca6bf7c..34228cf0b82 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -165,8 +165,8 @@ module IssuableCollections
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
when 'MergeRequest'
[
- :source_project, :target_project, :author, :assignee, :labels, :milestone,
- head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
+ :target_project, :author, :assignee, :labels, :milestone,
+ source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
end
end
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
new file mode 100644
index 00000000000..fb24edb8602
--- /dev/null
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -0,0 +1,31 @@
+class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
+ extend ::Gitlab::Utils::Override
+
+ def self.define_providers!
+ return unless Gitlab::Auth::LDAP::Config.enabled?
+
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
+ alias_method server['provider_name'], :ldap
+ end
+ end
+
+ # We only find ourselves here
+ # if the authentication to LDAP was successful.
+ def ldap
+ sign_in_user_flow(Gitlab::Auth::LDAP::User)
+ end
+
+ define_providers!
+
+ override :set_remember_me
+ def set_remember_me(user)
+ user.remember_me = params[:remember_me] if user.persisted?
+ end
+
+ override :fail_login
+ def fail_login(user)
+ flash[:alert] = 'Access denied for your LDAP account.'
+
+ redirect_to new_user_session_path
+ end
+end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 5e6676ea513..9137bc92810 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:kerberos, :saml, :cas3]
- Gitlab.config.omniauth.providers.each do |provider|
- define_method provider['name'] do
- handle_omniauth
- end
+ def handle_omniauth
+ omniauth_flow(Gitlab::Auth::OAuth)
end
- if Gitlab::Auth::LDAP::Config.enabled?
- Gitlab::Auth::LDAP::Config.available_servers.each do |server|
- define_method server['provider_name'] do
- ldap
- end
- end
+ Gitlab.config.omniauth.providers.each do |provider|
+ alias_method provider['name'], :handle_omniauth
end
# Extend the standard implementation to also increment
@@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
error ||= exception.error if exception.respond_to?(:error)
error ||= exception.message if exception.respond_to?(:message)
error ||= env["omniauth.error.type"].to_s
- error.to_s.humanize if error
- end
-
- # We only find ourselves here
- # if the authentication to LDAP was successful.
- def ldap
- ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
- ldap_user.save if ldap_user.changed? # will also save new users
-
- @user = ldap_user.gl_user
- @user.remember_me = params[:remember_me] if ldap_user.persisted?
- # Do additional LDAP checks for the user filter and EE features
- if ldap_user.allowed?
- if @user.two_factor_enabled?
- prompt_for_two_factor(@user)
- else
- log_audit_event(@user, with: oauth['provider'])
- sign_in_and_redirect(@user)
- end
- else
- fail_ldap_login
- end
+ error.to_s.humanize if error
end
def saml
- if current_user
- log_audit_event(current_user, with: :saml)
- # Update SAML identity if data has changed.
- identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
- if identity.nil?
- current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
- redirect_to profile_account_path, notice: 'Authentication method updated'
- else
- redirect_to after_sign_in_path_for(current_user)
- end
- else
- saml_user = Gitlab::Auth::Saml::User.new(oauth)
- saml_user.save if saml_user.changed?
- @user = saml_user.gl_user
-
- continue_login_process
- end
- rescue Gitlab::Auth::OAuth::User::SignupDisabledError
- handle_signup_error
+ omniauth_flow(Gitlab::Auth::Saml)
end
def omniauth_error
@@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
- def handle_omniauth
+ def omniauth_flow(auth_module, identity_linker: nil)
if current_user
- # Add new authentication method
- current_user.identities
- .with_extern_uid(oauth['provider'], oauth['uid'])
- .first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider'])
- redirect_to profile_account_path, notice: 'Authentication method updated'
- else
- oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
- oauth_user.save
- @user = oauth_user.gl_user
- continue_login_process
+ identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
+
+ identity_linker.link
+
+ if identity_linker.changed?
+ redirect_identity_linked
+ elsif identity_linker.error_message.present?
+ redirect_identity_link_failed(identity_linker.error_message)
+ else
+ redirect_identity_exists
+ end
+ else
+ sign_in_user_flow(auth_module::User)
end
- rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
- handle_disabled_provider
- rescue Gitlab::Auth::OAuth::User::SignupDisabledError
- handle_signup_error
+ end
+
+ def redirect_identity_exists
+ redirect_to after_sign_in_path_for(current_user)
+ end
+
+ def redirect_identity_link_failed(error_message)
+ redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
+ end
+
+ def redirect_identity_linked
+ redirect_to profile_account_path, notice: 'Authentication method updated'
end
def handle_service_ticket(provider, ticket)
@@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
- def continue_login_process
- # Only allow properly saved users to login.
- if @user.persisted? && @user.valid?
- log_audit_event(@user, with: oauth['provider'])
+ def sign_in_user_flow(auth_user_class)
+ auth_user = auth_user_class.new(oauth)
+ user = auth_user.find_and_update!
+
+ if auth_user.valid_sign_in?
+ log_audit_event(user, with: oauth['provider'])
- if @user.two_factor_enabled?
- params[:remember_me] = '1' if remember_me?
- prompt_for_two_factor(@user)
+ set_remember_me(user)
+
+ if user.two_factor_enabled?
+ prompt_for_two_factor(user)
else
- remember_me(@user) if remember_me?
- sign_in_and_redirect(@user)
+ sign_in_and_redirect(user)
end
else
- fail_login
+ fail_login(user)
end
+ rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
+ handle_disabled_provider
+ rescue Gitlab::Auth::OAuth::User::SignupDisabledError
+ handle_signup_error
end
def handle_signup_error
@@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
@oauth ||= request.env['omniauth.auth']
end
- def fail_login
- error_message = @user.errors.full_messages.to_sentence
+ def fail_login(user)
+ error_message = user.errors.full_messages.to_sentence
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end
- def fail_ldap_login
- flash[:alert] = 'Access denied for your LDAP account.'
-
- redirect_to new_user_session_path
- end
-
def fail_auth0_login
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
@@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
.for_authentication.security_event
end
+ def set_remember_me(user)
+ return unless remember_me?
+
+ if user.two_factor_enabled?
+ params[:remember_me] = '1'
+ else
+ remember_me(user)
+ end
+ end
+
def remember_me?
request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present?
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 454374121f3..94eef4ff7cd 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -31,7 +31,7 @@ module ProtectedRef
end
end
- def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil)
+ def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.check_access(user)
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c1c27ccf3e5..06aa67c600f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end
- def commits_count
- super || merge_request_diff_commits.size
- end
-
private
def create_merge_request_diff_files(diffs)
diff --git a/app/models/project.rb b/app/models/project.rb
index c293b0b8cf4..172980b0ad4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -68,6 +68,11 @@ class Project < ActiveRecord::Base
after_save :update_project_statistics, if: :namespace_id_changed?
after_create :create_project_feature, unless: :project_feature
+
+ after_create :create_ci_cd_settings,
+ unless: :ci_cd_settings,
+ if: proc { ProjectCiCdSetting.available? }
+
after_create :set_last_activity_at
after_create :set_last_repository_updated_at
after_update :update_forks_visibility_level
@@ -231,6 +236,7 @@ class Project < ActiveRecord::Base
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge'
+ has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
@@ -1041,13 +1047,6 @@ class Project < ActiveRecord::Base
"#{web_url}.git"
end
- def user_can_push_to_empty_repo?(user)
- return false unless empty_repo?
- return false unless Ability.allowed?(user, :push_code, self)
-
- !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
- end
-
def forked?
return true if fork_network && fork_network.root_project != self
@@ -2008,10 +2007,11 @@ class Project < ActiveRecord::Base
def fetch_branch_allows_maintainer_push?(user, branch_name)
check_access = -> do
+ next false if empty_repo?
+
merge_request = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.find_by(source_branch: branch_name)
-
merge_request&.can_be_merged_by?(user)
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
new file mode 100644
index 00000000000..9f10a93148c
--- /dev/null
+++ b/app/models/project_ci_cd_setting.rb
@@ -0,0 +1,16 @@
+class ProjectCiCdSetting < ActiveRecord::Base
+ belongs_to :project
+
+ # The version of the schema that first introduced this model/table.
+ MINIMUM_SCHEMA_VERSION = 20180403035759
+
+ def self.available?
+ @available ||=
+ ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
+ end
+
+ def self.reset_column_information
+ @available = nil
+ super
+ end
+end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 609780c5587..cb361a66591 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push
+ def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
+ # Masters, owners and admins are allowed to create the default branch
+ if default_branch_protected? && project.empty_repo?
+ return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
+ end
+
+ super
+ end
+
# Check if branch name is marked as protected in the system
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 63ead5538cb..ad655a7b3f4 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include StorageHelper
include TreeHelper
+ include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :project
@@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def can_current_user_push_to_branch?(branch)
- return false unless repository.branch_exists?(branch)
+ user_access(project).can_push_to_branch?(branch)
+ end
- ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
+ def can_current_user_push_to_default_branch?
+ can_current_user_push_to_branch?(default_branch)
end
def files_anchor_data
@@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def new_file_anchor_data
- if current_user && can_current_user_push_code?
+ if current_user && can_current_user_push_to_default_branch?
OpenStruct.new(enabled: false,
label: _('New file'),
link: project_new_blob_path(project, default_branch || 'master'),
@@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def readme_anchor_data
- if current_user && can_current_user_push_code? && repository.readme.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
OpenStruct.new(enabled: false,
label: _('Add Readme'),
link: add_readme_path)
@@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def changelog_anchor_data
- if current_user && can_current_user_push_code? && repository.changelog.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
OpenStruct.new(enabled: false,
label: _('Add Changelog'),
link: add_changelog_path)
@@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def license_anchor_data
- if current_user && can_current_user_push_code? && repository.license_blob.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
OpenStruct.new(enabled: false,
label: _('Add License'),
link: add_license_path)
@@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def contribution_guide_anchor_data
- if current_user && can_current_user_push_code? && repository.contribution_guide.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
OpenStruct.new(enabled: false,
label: _('Add Contribution guide'),
link: add_contribution_guide_path)
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 6cc51b6ee1b..0215994b1a7 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -138,8 +138,10 @@ module QuickActions
'Remove assignee'
end
end
- explanation do
- "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
+ explanation do |users = nil|
+ assignees = issuable.assignees
+ assignees &= users if users.present? && issuable.allows_multiple_assignees?
+ "Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
end
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
@@ -268,6 +270,26 @@ module QuickActions
end
end
+ desc 'Copy labels and milestone from other issue or merge request'
+ explanation do |source_issuable|
+ "Copy labels and milestone from #{source_issuable.to_reference}."
+ end
+ params '#issue | !merge_request'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ parse_params do |issuable_param|
+ extract_references(issuable_param, :issue).first ||
+ extract_references(issuable_param, :merge_request).first
+ end
+ command :copy_metadata do |source_issuable|
+ if source_issuable.present? && source_issuable.project.id == issuable.project.id
+ @updates[:add_label_ids] = source_issuable.labels.map(&:id)
+ @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
+ end
+ end
+
desc 'Add a todo'
explanation 'Adds a todo.'
condition do
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 86cd0759a2c..3375e01b3a1 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,4 +1,6 @@
- breadcrumb_title "General Settings"
+- @content_class = "limit-container-width" unless fluid_layout
+
.panel.panel-default.prepend-top-default
.panel-heading
Group settings
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 2f69da593cd..a066f9f4cca 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -58,7 +58,9 @@
touch README.md
git add README.md
git commit -m "add README"
- git push -u origin master
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
%fieldset
%h5 Existing folder
@@ -69,7 +71,9 @@
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add .
git commit -m "Initial commit"
- git push -u origin master
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
%fieldset
%h5 Existing Git repository
@@ -78,8 +82,10 @@
cd existing_repo
git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git push -u origin --all
- git push -u origin --tags
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin --all
+ git push -u origin --tags
- if can? current_user, :remove_project, @project
.prepend-top-20
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 475c6ba4d3d..a603b1024eb 100644..100755
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -12,7 +12,7 @@
- if @namespaces.present?
.fork-thumbnail-container.js-fork-content
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
- Click to fork the project
+ = _("Select a namespace to fork the project")
- @namespaces.each do |namespace|
= render 'fork_button', namespace: namespace
- else
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 2c80f7c3fa3..76f57320f99 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -29,7 +29,7 @@
docker login #{Gitlab.config.registry.host_port}
%br
%p
- - deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
+ - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
= s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br
%p
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index cb21f90696f..403d22c79f8 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -32,6 +32,13 @@
required: true,
title: 'You can choose a descriptive name different from the path.'
+- if @group.persisted?
+ .form-group.group-name-holder
+ = f.label :id, class: 'control-label' do
+ = _("Group ID")
+ .col-sm-10
+ = f.text_field :id, class: 'form-control', readonly: true
+
.form-group.group-description-holder
= f.label :description, class: 'control-label'
.col-sm-10
diff --git a/changelogs/unreleased/10244-add-project-ci-cd-settings.yml b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml
new file mode 100644
index 00000000000..89f9a0fe03c
--- /dev/null
+++ b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce new ProjectCiCdSetting model with group_runners_enabled
+merge_request: 18144
+author:
+type: performance
diff --git a/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml
new file mode 100644
index 00000000000..120e319acfb
--- /dev/null
+++ b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce queries on merge requests list page for merge requests from forks
+merge_request: 18561
+author:
+type: performance
diff --git a/changelogs/unreleased/45398-fix-rss-button.yml b/changelogs/unreleased/45398-fix-rss-button.yml
new file mode 100644
index 00000000000..2c8ee6f0564
--- /dev/null
+++ b/changelogs/unreleased/45398-fix-rss-button.yml
@@ -0,0 +1,5 @@
+---
+title: Fix tabs container styles to make RSS button clickable
+merge_request: 18559
+author:
+type: fixed
diff --git a/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml
new file mode 100644
index 00000000000..0b8c14ae699
--- /dev/null
+++ b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unassign slash command preview
+merge_request: 18447
+author:
+type: fixed
diff --git a/changelogs/unreleased/accessible-text.yml b/changelogs/unreleased/accessible-text.yml
new file mode 100755
index 00000000000..d39d5a9eb2c
--- /dev/null
+++ b/changelogs/unreleased/accessible-text.yml
@@ -0,0 +1,6 @@
+---
+title: Replace "Click" with "Select" to be more inclusive of people with accessibility
+ requirements
+merge_request: 18386
+author: Mark Lapierre
+type: other
diff --git a/changelogs/unreleased/add-copy-metadata-command.yml b/changelogs/unreleased/add-copy-metadata-command.yml
new file mode 100644
index 00000000000..3bf25ae6ce0
--- /dev/null
+++ b/changelogs/unreleased/add-copy-metadata-command.yml
@@ -0,0 +1,5 @@
+---
+title: Add Copy metadata quick action
+merge_request: 16473
+author: Mateusz Bajorski
+type: added
diff --git a/changelogs/unreleased/align-project-avatar-on-small-viewports.yml b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml
new file mode 100644
index 00000000000..320e7fbc294
--- /dev/null
+++ b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml
@@ -0,0 +1,5 @@
+---
+title: Align project avatar on small viewports
+merge_request: 18513
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-error.yml b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml
new file mode 100644
index 00000000000..66ab8fbf884
--- /dev/null
+++ b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors on pushing to an empty repository
+merge_request: 18462
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml
new file mode 100644
index 00000000000..b923f442b26
--- /dev/null
+++ b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml
@@ -0,0 +1,5 @@
+---
+title: Added Webhook SSRF prevention to documentation
+merge_request: 18532
+author:
+type: other
diff --git a/changelogs/unreleased/ide-file-finder.yml b/changelogs/unreleased/ide-file-finder.yml
new file mode 100644
index 00000000000..252dd3a30c4
--- /dev/null
+++ b/changelogs/unreleased/ide-file-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Added fuzzy file finder to web IDE
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/restore-label-underline-color.yml b/changelogs/unreleased/restore-label-underline-color.yml
new file mode 100644
index 00000000000..2e24ece5c36
--- /dev/null
+++ b/changelogs/unreleased/restore-label-underline-color.yml
@@ -0,0 +1,5 @@
+---
+title: Restore label underline color
+merge_request: 18407
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/sh-bump-lograge.yml b/changelogs/unreleased/sh-bump-lograge.yml
new file mode 100644
index 00000000000..65b15153a35
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-lograge.yml
@@ -0,0 +1,5 @@
+---
+title: Bump lograge to 0.10.0 and remove monkey patch
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/show-group-id-in-group-settings.yml b/changelogs/unreleased/show-group-id-in-group-settings.yml
new file mode 100644
index 00000000000..b975fe8c71d
--- /dev/null
+++ b/changelogs/unreleased/show-group-id-in-group-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Show group id in group settings
+merge_request: 18482
+author: George Tsiolis
+type: added
diff --git a/changelogs/unreleased/show-runners-description-on-jobs-page.yml b/changelogs/unreleased/show-runners-description-on-jobs-page.yml
new file mode 100644
index 00000000000..d9414a3d021
--- /dev/null
+++ b/changelogs/unreleased/show-runners-description-on-jobs-page.yml
@@ -0,0 +1,5 @@
+---
+title: Show Runner's description on job's page
+merge_request: 17321
+author:
+type: added
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 9b00ae459a3..575f27d1ea9 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -1,4 +1,4 @@
-require_dependency File.expand_path('../../lib/gitlab', __dir__) # Load Gitlab as soon as possible
+require_relative '../settings'
# Default settings
Settings['ldap'] ||= Settingslogic.new({})
diff --git a/config/initializers/2_gitlab.rb b/config/initializers/2_gitlab.rb
new file mode 100644
index 00000000000..1d2ab606a63
--- /dev/null
+++ b/config/initializers/2_gitlab.rb
@@ -0,0 +1 @@
+require_relative '../../lib/gitlab'
diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb
index f3f47b2ccf0..2476ea9e38a 100644
--- a/config/initializers/deprecations.rb
+++ b/config/initializers/deprecations.rb
@@ -1,5 +1,5 @@
deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
-if Gitlab.com? || Rails.env.development?
+if Gitlab.dev_env_or_com?
ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator)
end
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 49fdd23064c..114c1cb512f 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -1,21 +1,3 @@
-# Monkey patch lograge until https://github.com/roidrage/lograge/pull/241 is released
-module Lograge
- class RequestLogSubscriber < ActiveSupport::LogSubscriber
- def strip_query_string(path)
- index = path.index('?')
- index ? path[0, index] : path
- end
-
- def extract_location
- location = Thread.current[:lograge_location]
- return {} unless location
-
- Thread.current[:lograge_location] = nil
- { location: strip_query_string(location) }
- end
- end
-end
-
# Only use Lograge for Rails
unless Sidekiq.server?
filename = File.join(Rails.root, 'log', "#{Rails.env}_json.log")
diff --git a/config/karma.config.js b/config/karma.config.js
index 61f02294157..691cda98861 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -33,7 +33,7 @@ webpackConfig.plugins.push(
})
);
-webpackConfig.devtool = 'cheap-inline-source-map';
+webpackConfig.devtool = process.env.BABEL_ENV !== 'coverage' && 'cheap-inline-source-map';
// Karma configuration
module.exports = function(config) {
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 57fb37530bb..f8677693fab 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -1,3 +1,21 @@
+# Allows individual providers to be directed to a chosen controller
+# Call from inside devise_scope
+def override_omniauth(provider, controller, path_prefix = '/users/auth')
+ match "#{path_prefix}/#{provider}/callback",
+ to: "#{controller}##{provider}",
+ as: "#{provider}_omniauth_callback",
+ via: [:get, :post]
+end
+
+# Use custom controller for LDAP omniauth callback
+if Gitlab::Auth::LDAP::Config.enabled?
+ devise_scope :user do
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
+ override_omniauth(server['provider_name'], 'ldap/omniauth_callbacks')
+ end
+ end
+end
+
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
passwords: :passwords,
diff --git a/lib/settings.rb b/config/settings.rb
index 69d637761ea..69d637761ea 100644
--- a/lib/settings.rb
+++ b/config/settings.rb
diff --git a/db/migrate/20180403035759_create_project_ci_cd_settings.rb b/db/migrate/20180403035759_create_project_ci_cd_settings.rb
new file mode 100644
index 00000000000..06856af6204
--- /dev/null
+++ b/db/migrate/20180403035759_create_project_ci_cd_settings.rb
@@ -0,0 +1,68 @@
+class CreateProjectCiCdSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:project_ci_cd_settings)
+ create_table(:project_ci_cd_settings) do |t|
+ t.integer(:project_id, null: false)
+ t.boolean(:group_runners_enabled, default: true, null: false)
+ end
+ end
+
+ disable_statement_timeout
+
+ # This particular INSERT will take between 10 and 20 seconds.
+ execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects'
+
+ # We add the index and foreign key separately so the above INSERT statement
+ # takes as little time as possible.
+ add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true)
+
+ add_foreign_key_with_retry
+ end
+
+ def down
+ drop_table :project_ci_cd_settings
+ end
+
+ def add_foreign_key_with_retry
+ if Gitlab::Database.mysql?
+ # When using MySQL we don't support online upgrades, thus projects can't
+ # be deleted while we are running this migration.
+ return add_project_id_foreign_key
+ end
+
+ # Between the initial INSERT and the addition of the foreign key some
+ # projects may have been removed, leaving orphaned rows in our new settings
+ # table.
+ loop do
+ remove_orphaned_settings
+
+ begin
+ add_project_id_foreign_key
+ break
+ rescue ActiveRecord::InvalidForeignKey
+ say 'project_ci_cd_settings contains some orphaned rows, retrying...'
+ end
+ end
+ end
+
+ def add_project_id_foreign_key
+ add_concurrent_foreign_key(:project_ci_cd_settings, :projects, column: :project_id)
+ end
+
+ def remove_orphaned_settings
+ execute <<~SQL
+ DELETE FROM project_ci_cd_settings
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM projects
+ WHERE projects.id = project_ci_cd_settings.project_id
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb b/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb
new file mode 100644
index 00000000000..0e991c23bfa
--- /dev/null
+++ b/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb
@@ -0,0 +1,27 @@
+class AssureCommitsCountForMergeRequestDiff < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ include ::EachBatch
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('AddMergeRequestDiffCommitsCount')
+
+ MergeRequestDiff.where(commits_count: nil).each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount.new.perform(*range)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb
new file mode 100644
index 00000000000..3b0fdb3aeea
--- /dev/null
+++ b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateMissingProjectCiCdSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL does not support online upgrades, thus there can't be any missing
+ # rows.
+ return if Gitlab::Database.mysql?
+
+ # Projects created after the initial migration but before the code started
+ # using ProjectCiCdSetting won't have a corresponding row in
+ # project_ci_cd_settings, so let's fix that.
+ execute <<~SQL
+ INSERT INTO project_ci_cd_settings (project_id)
+ SELECT id
+ FROM projects
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM project_ci_cd_settings
+ WHERE project_ci_cd_settings.project_id = projects.id
+ )
+ SQL
+ end
+
+ def down
+ # There's nothing to revert for this migration.
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 9f371cdcb41..f9e4a017afa 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180420080616) do
+ActiveRecord::Schema.define(version: 20180425131009) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1437,6 +1437,13 @@ ActiveRecord::Schema.define(version: 20180420080616) do
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
+ create_table "project_ci_cd_settings", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.boolean "group_runners_enabled", default: true, null: false
+ end
+
+ add_index "project_ci_cd_settings", ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
+
create_table "project_custom_attributes", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -2163,6 +2170,7 @@ ActiveRecord::Schema.define(version: 20180420080616) do
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
+ add_foreign_key "project_ci_cd_settings", "projects", name: "fk_24c15d2f2e", on_delete: :cascade
add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index d8928a7fe4c..ad8ffc46559 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -75,43 +75,33 @@ Notice several options that you should consider using:
| `nobootwait` | Don't halt boot process waiting for this mount to become available
| `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously.
-## Mount locations
+## A single NFS mount
-When using default Omnibus configuration you will need to share 5 data locations
-between all GitLab cluster nodes. No other locations should be shared. The
-following are the 5 locations you need to mount:
-
-| Location | Description | Default configuration |
-| -------- | ----------- | --------------------- |
-| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
-| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
-| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
-| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
-| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
+It's recommended to nest all gitlab data dirs within a mount, that allows automatic
+restore of backups without manually moving existing data.
-Other GitLab directories should not be shared between nodes. They contain
-node-specific files and GitLab code that does not need to be shared. To ship
-logs to a central location consider using remote syslog. GitLab Omnibus packages
-provide configuration for [UDP log shipping][udp-log-shipping].
-
-### Consolidating mount points
-
-If you don't want to configure 5-6 different NFS mount points, you have a few
-alternative options.
+```
+mountpoint
+└── gitlab-data
+ ├── builds
+ ├── git-data
+ ├── home-git
+ ├── shared
+ └── uploads
+```
-#### Change default file locations
+To do so, we'll need to configure Omnibus with the paths to each directory nested
+in the mount point as follows:
-Omnibus allows you to configure the file locations. With custom configuration
-you can specify just one main mountpoint and have all of these locations
-as subdirectories. Mount `/gitlab-data` then use the following Omnibus
+Mount `/gitlab-nfs` then use the following Omnibus
configuration to move each data location to a subdirectory:
```ruby
-git_data_dirs({"default" => "/gitlab-data/git-data"})
-user['home'] = '/gitlab-data/home'
-gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
-gitlab_rails['shared_path'] = '/gitlab-data/shared'
-gitlab_ci['builds_directory'] = '/gitlab-data/builds'
+git_data_dirs({"default" => "/gitlab-nfs/gitlab-data/git-data"})
+user['home'] = '/gitlab-nfs/gitlab-data/home'
+gitlab_rails['uploads_directory'] = '/gitlab-nfs/gitlab-data/uploads'
+gitlab_rails['shared_path'] = '/gitlab-nfs/gitlab-data/shared'
+gitlab_ci['builds_directory'] = '/gitlab-nfs/gitlab-data/builds'
```
To move the `git` home directory, all GitLab services must be stopped. Run
@@ -122,22 +112,52 @@ Run `sudo gitlab-ctl reconfigure` to start using the central location. Please
be aware that if you had existing data you will need to manually copy/rsync it
to these new locations and then restart GitLab.
-#### Bind mounts
+## Bind mounts
+
+Alternatively to changing the configuration in Omnibus, bind mounts can be used
+to store the data on an NFS mount.
Bind mounts provide a way to specify just one NFS mount and then
bind the default GitLab data locations to the NFS mount. Start by defining your
single NFS mount point as you normally would in `/etc/fstab`. Let's assume your
-NFS mount point is `/gitlab-data`. Then, add the following bind mounts in
+NFS mount point is `/gitlab-nfs`. Then, add the following bind mounts in
`/etc/fstab`:
```bash
-/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0
-/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0
-/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0
-/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0
-/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0
+/gitlab-nfs/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0
+/gitlab-nfs/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0
+/gitlab-nfs/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0
+/gitlab-nfs/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0
+/gitlab-nfs/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0
```
+Using bind mounts will require manually making sure the data directories
+are empty before attempting a restore. Read more about the
+[restore prerequisites](../../raketasks/backup_restore.md).
+
+## Multiple NFS mounts
+
+When using default Omnibus configuration you will need to share 5 data locations
+between all GitLab cluster nodes. No other locations should be shared. The
+following are the 5 locations need to be shared:
+
+| Location | Description | Default configuration |
+| -------- | ----------- | --------------------- |
+| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
+| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
+| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
+| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
+| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
+
+Other GitLab directories should not be shared between nodes. They contain
+node-specific files and GitLab code that does not need to be shared. To ship
+logs to a central location consider using remote syslog. GitLab Omnibus packages
+provide configuration for [UDP log shipping][udp-log-shipping].
+
+Having multiple NFS mounts will require manually making sure the data directories
+are empty before attempting a restore. Read more about the
+[restore prerequisites](../../raketasks/backup_restore.md).
+
---
Read more on high-availability configuration:
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 60dc2ef9ac5..821413900fd 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -298,6 +298,28 @@ Mentioned briefly earlier, but the following things of Runners can be exploited.
We're always looking for contributions that can mitigate these
[Security Considerations](https://docs.gitlab.com/runner/security/).
+### Resetting the registration token for a Project
+
+If you think that registration token for a Project was revealed, you should
+reset them. It's recommended because such token can be used to register another
+Runner to thi Project. It may be next used to obtain the values of secret
+variables or clone the project code, that normally may be unavailable for the
+attacker.
+
+To reset the token:
+
+1. Go to **Settings > CI/CD** for a specified Project
+1. Expand the **General pipelines settings** section
+1. Find the **Runner token** form field and click the **Reveal value** button
+1. Delete the value and save the form
+1. After the page is refreshed, expand the **Runners settings** section
+ and check the registration token - it should be changed
+
+From now on the old token is not valid anymore and will not allow to register
+a new Runner to the project. If you are using any tools to provision and
+register new Runners, you should now update the token that is used to the
+new value.
+
## Determining the IP address of a Runner
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 0a6f402d5d2..af477f5ab99 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -126,13 +126,51 @@ it('tests a promise rejection', (done) => {
});
```
-#### Stubbing
+#### Stubbing and Mocking
-For unit tests, you should stub methods that are unrelated to the current unit you are testing.
-If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
+Jasmine provides useful helpers `spyOn`, `spyOnProperty`, `jasmine.createSpy`,
+and `jasmine.createSpyObject` to facilitate replacing methods with dummy
+placeholders, and recalling when they are called and the arguments that are
+passed to them. These tools should be used liberally, to test for expected
+behavior, to mock responses, and to block unwanted side effects (such as a
+method that would generate a network request or alter `window.location`). The
+documentation for these methods can be found in the [jasmine introduction page](https://jasmine.github.io/2.0/introduction.html#section-Spies).
-For integration tests, you should stub methods that will effect the stability of the test if they
-execute their original behaviour. i.e. Network requests.
+Sometimes you may need to spy on a method that is directly imported by another
+module. GitLab has a custom `spyOnDependency` method which utilizes
+[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
+achieve this. It can be used like so:
+
+```javascript
+// my_module.js
+import { visitUrl } from '~/lib/utils/url_utility';
+
+export default function doSomething() {
+ visitUrl('/foo/bar');
+}
+
+// my_module_spec.js
+import doSomething from '~/my_module';
+
+describe('my_module', () => {
+ it('does something', () => {
+ const visitUrl = spyOnDependency(doSomething, 'visitUrl');
+
+ doSomething();
+ expect(visitUrl).toHaveBeenCalledWith('/foo/bar');
+ });
+});
+```
+
+Unlike `spyOn`, `spyOnDependency` expects its first parameter to be the default
+export of a module who's import you want to stub, rather than an object which
+contains a method you wish to stub (if the module does not have a default
+export, one is be generated by the babel plugin). The second parameter is the
+name of the import you wish to change. The result of the function is a Spy
+object which can be treated like any other jasmine spy object.
+
+Further documentation on the babel rewire pluign API can be found on
+[its repository Readme doc](https://github.com/speedskater/babel-plugin-rewire#babel-plugin-rewire).
### Vue.js unit tests
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index bbd2d214fe4..785cc32d590 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -498,6 +498,13 @@ more of the following options:
Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed.
+If you are restoring into directories that are mountpoints you will need to make
+sure these directories are empty before attempting a restore. Otherwise GitLab
+will attempt to move these directories before restoring the new data and this
+would cause an error.
+
+Read more on [configuring NFS mounts](../administration/high_availability/nfs.md)
+
### Restore for installation from source
```
diff --git a/doc/security/img/outbound_requests_section.png b/doc/security/img/outbound_requests_section.png
new file mode 100644
index 00000000000..95c9c6ee771
--- /dev/null
+++ b/doc/security/img/outbound_requests_section.png
Binary files differ
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index faabc53ce72..a573445ab5b 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -2,12 +2,19 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
-If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
+If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
-To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
+To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed.
+
+This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`):
+
+![Outbound requests admin settings](img/outbound_requests_section.png)
+
+>**Note:**
+*System hooks* are exempt from this protection because they are set up by admins.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 442fc978284..2f4ed3493c2 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -38,6 +38,7 @@ do.
| `/award :emoji:` | Toggle award for :emoji: |
| `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
-| `/move path/to/project` | Moves issue to another project |
-| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
-| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | \ No newline at end of file
+| `/move path/to/project` | Moves issue to another project |
+| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
+| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
+| <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request |
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index eb0ac221e30..2c90f4b4413 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -31,7 +31,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.4 to current | 0.2.2 |
+| 10.8 to current | 0.2.3 |
+| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 377eee69c11..104ac0cf31b 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -244,3 +244,20 @@ GitLab checks files to detect LFS pointers on push. If LFS pointers are detected
Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
+
+### Hosting LFS objects externally
+
+It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
+
+Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
+
+LFS can be disabled for a project by Owners and Masters using the [Project API](../../api/projects.md#edit-project).
+
+```bash
+curl --request PUT \
+ --url https://example.com/api/v4/projects/<PROJECT_ID> \
+ --header 'Private-Token: <YOUR_PRIVATE_TOKEN>' \
+ --data 'lfs_enabled=false'
+```
+
+Note, `<PROJECT_ID>` can also be substituted with a [namespaced path](../../api/README.md#namespaced-path-encoding).
diff --git a/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb
new file mode 100644
index 00000000000..f1e851a210b
--- /dev/null
+++ b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb
@@ -0,0 +1,22 @@
+module EE
+ module Ldap
+ module OmniauthCallbacksController
+ extend ::Gitlab::Utils::Override
+
+ override :sign_in_and_redirect
+ def sign_in_and_redirect(user)
+ # The counter gets incremented in `sign_in_and_redirect`
+ show_ldap_sync_flash if user.sign_in_count == 0
+
+ super
+ end
+
+ private
+
+ def show_ldap_sync_flash
+ flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\
+ 'Refresh the page to see the changes.'
+ end
+ end
+ end
+end
diff --git a/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
new file mode 100644
index 00000000000..0835ff35846
--- /dev/null
+++ b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Ldap::OmniauthCallbacksController do
+ include_context 'Ldap::OmniauthCallbacksController'
+
+ it "displays LDAP sync flash on first sign in" do
+ post provider
+
+ expect(flash[:notice]).to match(/LDAP sync in progress*/)
+ end
+
+ it "skips LDAP sync flash on subsequent sign ins" do
+ user.update!(sign_in_count: 1)
+
+ post provider
+
+ expect(flash[:notice]).to eq nil
+ end
+
+ context 'access denied' do
+ let(:valid_login?) { false }
+
+ it 'logs a failure event' do
+ stub_licensed_features(extended_audit_events: true)
+
+ expect { post provider }.to change(SecurityEvent, :count).by(1)
+ end
+ end
+end
diff --git a/features/project/find_file.feature b/features/project/find_file.feature
deleted file mode 100644
index ae8fa245923..00000000000
--- a/features/project/find_file.feature
+++ /dev/null
@@ -1,42 +0,0 @@
-@dashboard
-Feature: Project Find File
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's files page
-
- @javascript
- Scenario: Navigate to find file by shortcut
- Given I press "t"
- Then I should see "find file" page
-
- Scenario: Navigate to find file
- Given I click Find File button
- Then I should see "find file" page
-
- @javascript
- Scenario: I search file
- Given I visit project find file page
- And I fill in file find with "change"
- Then I should not see ".gitignore" in files
- And I should not see ".gitmodules" in files
- And I should see "CHANGELOG" in files
- And I should not see "VERSION" in files
-
- @javascript
- Scenario: I search file that not exist
- Given I visit project find file page
- And I fill in file find with "asdfghjklqwertyuizxcvbnm"
- Then I should not see ".gitignore" in files
- And I should not see ".gitmodules" in files
- And I should not see "CHANGELOG" in files
- And I should not see "VERSION" in files
-
- @javascript
- Scenario: I search file that partially matches
- Given I visit project find file page
- And I fill in file find with "git"
- Then I should see ".gitignore" in files
- And I should see ".gitmodules" in files
- And I should not see "CHANGELOG" in files
- And I should not see "VERSION" in files
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
deleted file mode 100644
index 461160b8430..00000000000
--- a/features/steps/project/project_find_file.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedProjectTab
-
- step 'I press "t"' do
- find('body').native.send_key('t')
- end
-
- step 'I click Find File button' do
- click_link 'Find file'
- end
-
- step 'I should see "find file" page' do
- ensure_active_main_tab('Repository')
- expect(page).to have_selector('.file-finder-holder', count: 1)
- end
-
- step 'I fill in Find by path with "git"' do
- ensure_active_main_tab('Repository')
- expect(page).to have_selector('.file-finder-holder', count: 1)
- end
-
- step 'I fill in file find with "git"' do
- find_file "git"
- end
-
- step 'I fill in file find with "change"' do
- find_file "change"
- end
-
- step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do
- find_file "asdfghjklqwertyuizxcvbnm"
- end
-
- step 'I should see "VERSION" in files' do
- expect(page).to have_content("VERSION")
- end
-
- step 'I should not see "VERSION" in files' do
- expect(page).not_to have_content("VERSION")
- end
-
- step 'I should see "CHANGELOG" in files' do
- expect(page).to have_content("CHANGELOG")
- end
-
- step 'I should not see "CHANGELOG" in files' do
- expect(page).not_to have_content("CHANGELOG")
- end
-
- step 'I should see ".gitmodules" in files' do
- expect(page).to have_content(".gitmodules")
- end
-
- step 'I should not see ".gitmodules" in files' do
- expect(page).not_to have_content(".gitmodules")
- end
-
- step 'I should see ".gitignore" in files' do
- expect(page).to have_content(".gitignore")
- end
-
- step 'I should not see ".gitignore" in files' do
- expect(page).not_to have_content(".gitignore")
- end
-
- def find_file(text)
- fill_in 'file_find', with: text
- end
-end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index d16c127f6e6..014e6ad625b 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -200,10 +200,6 @@ module SharedPaths
visit edit_project_path(@project)
end
- step "I visit my project's files page" do
- visit project_tree_path(@project, root_ref)
- end
-
step 'I visit a binary file in the repo' do
visit project_blob_path(@project,
File.join(root_ref, 'files/images/logo-black.png'))
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 88cb7e7b5a4..9895db9e451 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -53,6 +53,8 @@ module Backup
FileUtils.mv(files, timestamped_files_path)
rescue Errno::EACCES
access_denied_error(app_files_dir)
+ rescue Errno::EBUSY
+ resource_busy_error(app_files_dir)
end
end
end
diff --git a/lib/backup/helper.rb b/lib/backup/helper.rb
index a1ee0faefe9..54b9ce10b4d 100644
--- a/lib/backup/helper.rb
+++ b/lib/backup/helper.rb
@@ -13,5 +13,19 @@ module Backup
EOS
raise message
end
+
+ def resource_busy_error(path)
+ message = <<~EOS
+
+ ### NOTICE ###
+ As part of restore, the task tried to rename `#{path}` before restoring.
+ This could not be completed, perhaps `#{path}` is a mountpoint?
+
+ To complete the restore, please move the contents of `#{path}` to a
+ different location and run the restore task again.
+
+ EOS
+ raise message
+ end
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 89e3f1d9076..65e06fd78c0 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -81,6 +81,8 @@ module Backup
FileUtils.mv(files, bk_repos_path)
rescue Errno::EACCES
access_denied_error(path)
+ rescue Errno::EBUSY
+ resource_busy_error(path)
end
end
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 0a167104bf4..c5498d0da1a 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -1,4 +1,3 @@
-require_dependency 'settings'
require_dependency 'gitlab/popen'
module Gitlab
@@ -30,6 +29,6 @@ module Gitlab
end
def self.dev_env_or_com?
- Rails.env.test? || Rails.env.development? || org? || com?
+ Rails.env.development? || org? || com?
end
end
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
index 068212d9a21..922d0567d99 100644
--- a/lib/gitlab/auth/ldap/user.rb
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -8,6 +8,8 @@ module Gitlab
module Auth
module LDAP
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
class << self
def find_by_uid_and_provider(uid, provider)
identity = ::Identity.with_extern_uid(provider, uid).take
@@ -29,7 +31,8 @@ module Gitlab
self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
- def changed?
+ override :should_save?
+ def should_save?
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
@@ -41,6 +44,10 @@ module Gitlab
Gitlab::Auth::LDAP::Access.allowed?(gl_user)
end
+ def valid_sign_in?
+ allowed? && super
+ end
+
def ldap_config
Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
end
diff --git a/lib/gitlab/auth/o_auth/identity_linker.rb b/lib/gitlab/auth/o_auth/identity_linker.rb
new file mode 100644
index 00000000000..de92d7a214d
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module OAuth
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index d0c6b0386ba..6c5d0788a0a 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -30,6 +30,10 @@ module Gitlab
gl_user.try(:valid?)
end
+ def valid_sign_in?
+ valid? && persisted?
+ end
+
def save(provider = 'OAuth')
raise SigninDisabledForProviderError if oauth_provider_disabled?
raise SignupDisabledError unless gl_user
@@ -64,8 +68,18 @@ module Gitlab
user
end
+ def find_and_update!
+ save if should_save?
+
+ gl_user
+ end
+
protected
+ def should_save?
+ true
+ end
+
def add_or_update_user_identities
return unless gl_user
diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb
new file mode 100644
index 00000000000..ae365fcdfaa
--- /dev/null
+++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Auth
+ class OmniauthIdentityLinkerBase
+ attr_reader :current_user, :oauth
+
+ def initialize(current_user, oauth)
+ @current_user = current_user
+ @oauth = oauth
+ @changed = false
+ end
+
+ def link
+ save if identity.new_record?
+ end
+
+ def changed?
+ @changed
+ end
+
+ def error_message
+ identity.validate
+
+ identity.errors.full_messages.join(', ')
+ end
+
+ private
+
+ def save
+ @changed = identity.save
+ end
+
+ def identity
+ @identity ||= current_user.identities
+ .with_extern_uid(provider, uid)
+ .first_or_initialize(extern_uid: uid)
+ end
+
+ def provider
+ oauth['provider']
+ end
+
+ def uid
+ oauth['uid']
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/identity_linker.rb b/lib/gitlab/auth/saml/identity_linker.rb
new file mode 100644
index 00000000000..7e4b191d512
--- /dev/null
+++ b/lib/gitlab/auth/saml/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module Saml
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index d4024e9ec39..cb01cd8004c 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -7,6 +7,8 @@ module Gitlab
module Auth
module Saml
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
def save
super('SAML')
end
@@ -21,13 +23,14 @@ module Gitlab
if external_users_enabled? && user
# Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal.
- user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty?
+ user.external = !(auth_hash.groups & saml_config.external_groups).empty?
end
user
end
- def changed?
+ override :should_save?
+ def should_save?
return true unless gl_user
gl_user.changed? || gl_user.identities.any?(&:changed?)
@@ -35,12 +38,16 @@ module Gitlab
protected
+ def saml_config
+ Gitlab::Auth::Saml::Config
+ end
+
def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user
end
def external_users_enabled?
- !Gitlab::Auth::Saml::Config.external_groups.nil?
+ !saml_config.external_groups.nil?
end
def auth_hash=(auth_hash)
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index af203ff711d..b713fa7e1cd 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.2'.freeze
+ VERSION = '0.2.3'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index ec91c02dbe7..0d1c4f73c6e 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -64,6 +64,7 @@ project_tree:
- :project_feature
- :custom_attributes
- :project_badges
+ - :ci_cd_settings
# Only include the following attributes for the models specified.
included_attributes:
@@ -73,6 +74,8 @@ included_attributes:
- :username
author:
- :name
+ ci_cd_settings:
+ - :group_runners_enabled
# Do not include the following attributes for the models specified.
excluded_attributes:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 598832fb2df..e3e9f156fb4 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -17,7 +17,8 @@ module Gitlab
auto_devops: :project_auto_devops,
label: :project_label,
custom_attributes: 'ProjectCustomAttribute',
- project_badges: 'Badge' }.freeze
+ project_badges: 'Badge',
+ ci_cd_settings: 'ProjectCiCdSetting' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 69952cbb47c..8cf5d636743 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -63,10 +63,12 @@ module Gitlab
request_cache def can_push_to_branch?(ref)
return false unless can_access_git?
- return false unless user.can?(:push_code, project) || project.branch_allows_maintainer_push?(user, ref)
+ return false unless project
+
+ return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref)
if protected?(ProtectedBranch, project, ref)
- project.user_can_push_to_empty_repo?(user) || protected_branch_accessible_to?(ref, action: :push)
+ protected_branch_accessible_to?(ref, action: :push)
else
true
end
@@ -101,6 +103,7 @@ module Gitlab
def protected_branch_accessible_to?(ref, action:)
ProtectedBranch.protected_ref_accessible_to?(
ref, user,
+ project: project,
action: action,
protected_refs: project.protected_branches)
end
@@ -108,6 +111,7 @@ module Gitlab
def protected_tag_accessible_to?(ref, action:)
ProtectedTag.protected_ref_accessible_to?(
ref, user,
+ project: project,
action: action,
protected_refs: project.protected_tags)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cd30783c274..17917b1176f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-17 11:44+0200\n"
-"PO-Revision-Date: 2018-04-17 11:44+0200\n"
+"POT-Creation-Date: 2018-04-24 13:19+0000\n"
+"PO-Revision-Date: 2018-04-24 13:19+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -3136,6 +3136,9 @@ msgstr ""
msgid "Select Archive Format"
msgstr ""
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr ""
diff --git a/package.json b/package.json
index 45bea12fd9b..96dee321548 100644
--- a/package.json
+++ b/package.json
@@ -5,9 +5,9 @@
"eslint": "eslint --max-warnings 0 --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
- "karma": "karma start --single-run true config/karma.config.js",
+ "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
- "karma-start": "karma start config/karma.config.js",
+ "karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
"prettier-staged": "node ./scripts/frontend/prettier.js",
"prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all",
@@ -88,6 +88,7 @@
"vue-resource": "^1.3.5",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.13",
+ "vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
"webpack": "^3.11.0",
"webpack-bundle-analyzer": "^2.10.0",
@@ -98,6 +99,9 @@
"axios-mock-adapter": "^1.10.0",
"babel-eslint": "^8.0.2",
"babel-plugin-istanbul": "^4.1.5",
+ "babel-plugin-rewire": "^1.1.0",
+ "babel-template": "^6.26.0",
+ "babel-types": "^6.26.0",
"commander": "^2.15.1",
"eslint": "^3.18.0",
"eslint-config-airbnb-base": "^10.0.1",
diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
new file mode 100644
index 00000000000..87c10a86cdd
--- /dev/null
+++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Ldap::OmniauthCallbacksController do
+ include_context 'Ldap::OmniauthCallbacksController'
+
+ it 'allows sign in' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+
+ it 'respects remember me checkbox' do
+ expect do
+ post provider, remember_me: '1'
+ end.to change { user.reload.remember_created_at }.from(nil)
+ end
+
+ context 'with 2FA' do
+ let(:user) { create(:omniauth_user, :two_factor_via_otp, extern_uid: uid, provider: provider) }
+
+ it 'passes remember_me to the Devise view' do
+ post provider, remember_me: '1'
+
+ expect(assigns[:user].remember_me).to eq '1'
+ end
+ end
+
+ context 'access denied' do
+ let(:valid_login?) { false }
+
+ it 'warns the user' do
+ post provider
+
+ expect(flash[:alert]).to match(/Access denied for your LDAP account*/)
+ end
+
+ it "doesn't authenticate user" do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'sign up' do
+ let(:user) { double(email: 'new@example.com') }
+
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
+
+ it 'is allowed' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1ae6152a1f0..803498d3b19 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -151,6 +151,13 @@ FactoryBot.define do
end
end
+ trait :stubbed_repository do
+ after(:build) do |project|
+ allow(project).to receive(:empty_repo?).and_return(false)
+ allow(project.repository).to receive(:empty?).and_return(false)
+ end
+ end
+
trait :wiki_repo do
after(:create) do |project|
raise 'Failed to create wiki repository!' unless project.create_wiki
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index f28268b0754..978113a08a4 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -9,7 +9,8 @@ unless Object.respond_to?(:require_dependency)
end
end
-# Defines Gitlab and Gitlab.config which are at the center of the app
+# Defines Settings and Gitlab.config which are at the center of the app
+require_relative '../config/settings'
require_relative '../lib/gitlab' unless defined?(Gitlab.config)
require_relative 'support/rspec'
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index a5e325ee2e3..013cdaa6479 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -28,35 +28,46 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
OmniAuth.config.full_host = @omniauth_config_full_host
end
+ def login_with_provider(provider, enter_two_factor: false)
+ login_via(provider.to_s, user, uid, remember_me: remember_me)
+ enter_code(user.current_otp) if enter_two_factor
+ end
+
providers.each do |provider|
context "when the user logs in using the #{provider} provider" do
+ let(:uid) { 'my-uid' }
+ let(:remember_me) { false }
+ let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider.to_s) }
+ let(:two_factor_user) { create(:omniauth_user, :two_factor, extern_uid: uid, provider: provider.to_s) }
+
+ before do
+ stub_omniauth_config(provider)
+ end
+
context 'when two-factor authentication is disabled' do
it 'logs the user in' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid')
+ login_with_provider(provider)
expect(current_path).to eq root_path
end
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'logs the user in' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid')
+ login_with_provider(provider, enter_two_factor: true)
- enter_code(user.current_otp)
expect(current_path).to eq root_path
end
end
context 'when "remember me" is checked' do
+ let(:remember_me) { true }
+
context 'when two-factor authentication is disabled' do
it 'remembers the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: true)
+ login_with_provider(provider)
clear_browser_session
@@ -66,11 +77,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'remembers the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: true)
- enter_code(user.current_otp)
+ login_with_provider(provider, enter_two_factor: true)
clear_browser_session
@@ -83,9 +93,7 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
context 'when "remember me" is not checked' do
context 'when two-factor authentication is disabled' do
it 'does not remember the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: false)
+ login_with_provider(provider)
clear_browser_session
@@ -95,11 +103,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'does not remember the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: false)
- enter_code(user.current_otp)
+ login_with_provider(provider, enter_two_factor: true)
clear_browser_session
diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
new file mode 100644
index 00000000000..b7d063596c1
--- /dev/null
+++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+feature 'User creates blob in new project', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :empty_repo) }
+
+ shared_examples 'creating a file' do
+ before do
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'allows the user to add a new file' do
+ click_link 'New file'
+
+ find('#editor')
+ execute_script('ace.edit("editor").setValue("Hello world")')
+
+ fill_in(:file_name, with: 'dummy-file')
+
+ click_button('Commit changes')
+
+ expect(page).to have_content('The file has been successfully created')
+ end
+ end
+
+ describe 'as a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'creating a file'
+ end
+
+ describe 'as an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it_behaves_like 'creating a file'
+ end
+
+ describe 'as a developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'does not allow pushing to the default branch' do
+ expect(page).not_to have_content('New file')
+ end
+ end
+end
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
new file mode 100644
index 00000000000..df405e70dd4
--- /dev/null
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe 'User find project file' do
+ let(:user) { create :user }
+ let(:project) { create :project, :repository }
+
+ before do
+ sign_in(user)
+ project.add_master(user)
+
+ visit project_tree_path(project, project.repository.root_ref)
+ end
+
+ def active_main_tab
+ find('.sidebar-top-level-items > li.active')
+ end
+
+ def find_file(text)
+ fill_in 'file_find', with: text
+ end
+
+ it 'navigates to find file by shortcut', :js do
+ find('body').native.send_key('t')
+
+ expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ it 'navigates to find file' do
+ click_link 'Find file'
+
+ expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ it 'searches CHANGELOG file', :js do
+ click_link 'Find file'
+
+ find_file 'change'
+
+ expect(page).to have_content('CHANGELOG')
+ expect(page).not_to have_content('.gitignore')
+ expect(page).not_to have_content('VERSION')
+ end
+
+ it 'does not find file when search not exist file', :js do
+ click_link 'Find file'
+
+ find_file 'asdfghjklqwertyuizxcvbnm'
+
+ expect(page).not_to have_content('CHANGELOG')
+ expect(page).not_to have_content('.gitignore')
+ expect(page).not_to have_content('VERSION')
+ end
+
+ it 'searches file by partially matches', :js do
+ click_link 'Find file'
+
+ find_file 'git'
+
+ expect(page).to have_content('.gitignore')
+ expect(page).to have_content('.gitmodules')
+ expect(page).not_to have_content('CHANGELOG')
+ expect(page).not_to have_content('VERSION')
+ end
+end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index ecb7651acad..72ab2d71f35 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
new file mode 100644
index 00000000000..7b982301ffc
--- /dev/null
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe 'User views an empty project' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:user) { create(:user) }
+
+ shared_examples 'allowing push to default branch' do
+ before do
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'shows push-to-master instructions' do
+ expect(page).to have_content('git push -u origin master')
+ end
+ end
+
+ describe 'as a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'allowing push to default branch'
+ end
+
+ describe 'as an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it_behaves_like 'allowing push to default branch'
+ end
+
+ describe 'as a developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'does not show push-to-master instructions' do
+ expect(page).not_to have_content('git push -u origin master')
+ end
+ end
+end
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
index 352384f16c8..bef7e2ff8ee 100644
--- a/spec/fixtures/exported-project.gz
+++ b/spec/fixtures/exported-project.gz
Binary files differ
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index 3d922021978..9eb0e732572 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -18,6 +18,7 @@
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
+ "spyOnDependency": false,
"spyOnEvent": false,
"ClassSpecHelper": false
},
diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js
index 909a1bf76bc..5dbdcd24296 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/javascripts/activities_spec.js
@@ -3,24 +3,30 @@
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
import Activities from '~/activities';
+import Pager from '~/pager';
-(() => {
+describe('Activities', () => {
window.gon || (window.gon = {});
const fixtureTemplate = 'static/event_filter.html.raw';
const filters = [
{
id: 'all',
- }, {
+ },
+ {
id: 'push',
name: 'push events',
- }, {
+ },
+ {
id: 'merged',
name: 'merge events',
- }, {
+ },
+ {
id: 'comments',
- }, {
+ },
+ {
id: 'team',
- }];
+ },
+ ];
function getEventName(index) {
const filter = filters[index];
@@ -32,31 +38,34 @@ import Activities from '~/activities';
return `#${filter.id}_event_filter`;
}
- describe('Activities', () => {
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- new Activities();
- });
-
- for (let i = 0; i < filters.length; i += 1) {
- ((i) => {
- describe(`when selecting ${getEventName(i)}`, () => {
- beforeEach(() => {
- $(getSelector(i)).click();
- });
-
- for (let x = 0; x < filters.length; x += 1) {
- ((x) => {
- const shouldHighlight = i === x;
- const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
-
- it(`${testName} ${getEventName(x)}`, () => {
- expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
- });
- })(x);
- }
- });
- })(i);
- }
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ spyOn(Pager, 'init').and.stub();
+ new Activities();
});
-})();
+
+ for (let i = 0; i < filters.length; i += 1) {
+ (i => {
+ describe(`when selecting ${getEventName(i)}`, () => {
+ beforeEach(() => {
+ $(getSelector(i)).click();
+ });
+
+ for (let x = 0; x < filters.length; x += 1) {
+ (x => {
+ const shouldHighlight = i === x;
+ const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+
+ it(`${testName} ${getEventName(x)}`, () => {
+ expect(
+ $(getSelector(x))
+ .parent()
+ .hasClass('active'),
+ ).toEqual(shouldHighlight);
+ });
+ })(x);
+ }
+ });
+ })(i);
+ }
+});
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index c37c62c63dd..d03836d10f9 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import '~/behaviors/quick_submit';
-describe('Quick Submit behavior', () => {
+describe('Quick Submit behavior', function () {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
index 0b1de504435..346f795c3f5 100644
--- a/spec/javascripts/blob/blob_file_dropzone_spec.js
+++ b/spec/javascripts/blob/blob_file_dropzone_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import BlobFileDropzone from '~/blob/blob_file_dropzone';
-describe('BlobFileDropzone', () => {
+describe('BlobFileDropzone', function () {
preloadFixtures('blob/show.html.raw');
beforeEach(() => {
diff --git a/spec/javascripts/comment_type_toggle_spec.js b/spec/javascripts/comment_type_toggle_spec.js
index dfd0810d52e..0ba709298c5 100644
--- a/spec/javascripts/comment_type_toggle_spec.js
+++ b/spec/javascripts/comment_type_toggle_spec.js
@@ -1,5 +1,4 @@
import CommentTypeToggle from '~/comment_type_toggle';
-import * as dropLabSrc from '~/droplab/drop_lab';
import InputSetter from '~/droplab/plugins/input_setter';
describe('CommentTypeToggle', function () {
@@ -59,14 +58,14 @@ describe('CommentTypeToggle', function () {
this.droplab = jasmine.createSpyObj('droplab', ['init']);
- spyOn(dropLabSrc, 'default').and.returnValue(this.droplab);
+ this.droplabConstructor = spyOnDependency(CommentTypeToggle, 'DropLab').and.returnValue(this.droplab);
spyOn(this.commentTypeToggle, 'setConfig').and.returnValue(this.config);
CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle);
});
it('should instantiate a DropLab instance', function () {
- expect(dropLabSrc.default).toHaveBeenCalled();
+ expect(this.droplabConstructor).toHaveBeenCalled();
});
it('should set .droplab', function () {
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 53820770f3f..819ed7896ca 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-describe('Pipelines table in Commits and Merge requests', () => {
+describe('Pipelines table in Commits and Merge requests', function () {
const jsonFixtureName = 'pipelines/pipelines.json';
let pipeline;
let PipelinesTable;
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index 977298b9221..60d100e8544 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -3,6 +3,7 @@ import 'vendor/jquery.endless-scroll';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import CommitsList from '~/commits';
+import Pager from '~/pager';
describe('Commits List', () => {
let commitsList;
@@ -14,6 +15,7 @@ describe('Commits List', () => {
</form>
<ol id="commits-list"></ol>
`);
+ spyOn(Pager, 'init').and.stub();
commitsList = new CommitsList(25);
});
@@ -68,9 +70,10 @@ describe('Commits List', () => {
mock.restore();
});
- it('should save the last search string', (done) => {
+ it('should save the last search string', done => {
commitsList.searchField.val('GitLab');
- commitsList.filterResults()
+ commitsList
+ .filterResults()
.then(() => {
expect(ajaxSpy).toHaveBeenCalled();
expect(commitsList.lastSearch).toEqual('GitLab');
@@ -80,8 +83,9 @@ describe('Commits List', () => {
.catch(done.fail);
});
- it('should not make ajax call if the input does not change', (done) => {
- commitsList.filterResults()
+ it('should not make ajax call if the input does not change', done => {
+ commitsList
+ .filterResults()
.then(() => {
expect(ajaxSpy).not.toHaveBeenCalled();
expect(commitsList.lastSearch).toEqual('');
diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js
index 3d39bd0812b..5eed1db2750 100644
--- a/spec/javascripts/droplab/hook_spec.js
+++ b/spec/javascripts/droplab/hook_spec.js
@@ -1,5 +1,4 @@
import Hook from '~/droplab/hook';
-import * as dropdownSrc from '~/droplab/drop_down';
describe('Hook', function () {
describe('class constructor', function () {
@@ -10,7 +9,7 @@ describe('Hook', function () {
this.config = {};
this.dropdown = {};
- spyOn(dropdownSrc, 'default').and.returnValue(this.dropdown);
+ this.dropdownConstructor = spyOnDependency(Hook, 'DropDown').and.returnValue(this.dropdown);
this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
});
@@ -24,7 +23,7 @@ describe('Hook', function () {
});
it('should call DropDown constructor', function () {
- expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config);
+ expect(this.dropdownConstructor).toHaveBeenCalledWith(this.list, this.config);
});
it('should set .type', function () {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 95d02974bdc..8fcee36beb8 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,5 +1,3 @@
-import * as urlUtils from '~/lib/utils/url_utility';
-import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
@@ -11,7 +9,7 @@ import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dro
import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
-describe('Filtered Search Manager', () => {
+describe('Filtered Search Manager', function () {
let input;
let manager;
let tokensContainer;
@@ -74,18 +72,19 @@ describe('Filtered Search Manager', () => {
describe('class constructor', () => {
const isLocalStorageAvailable = 'isLocalStorageAvailable';
+ let RecentSearchesStoreSpy;
beforeEach(() => {
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
- spyOn(recentSearchesStoreSrc, 'default');
spyOn(RecentSearchesRoot.prototype, 'render');
+ RecentSearchesStoreSpy = spyOnDependency(FilteredSearchManager, 'RecentSearchesStore');
});
it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
manager = new FilteredSearchManager({ page });
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
- expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({
+ expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({
isLocalStorageAvailable,
allowedKeys: FilteredSearchTokenKeys.getKeys(),
});
@@ -164,7 +163,7 @@ describe('Filtered Search Manager', () => {
it('should search with a single word', (done) => {
input.value = 'searchTerm';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
});
@@ -175,7 +174,7 @@ describe('Filtered Search Manager', () => {
it('should search with multiple words', (done) => {
input.value = 'awesome search terms';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
});
@@ -186,7 +185,7 @@ describe('Filtered Search Manager', () => {
it('should search with special characters', (done) => {
input.value = '~!@#$%^&*()_+{}:<>,.?/';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
});
@@ -200,7 +199,7 @@ describe('Filtered Search Manager', () => {
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
index d8ba6de5f45..1e6272bad0b 100644
--- a/spec/javascripts/filtered_search/recent_searches_root_spec.js
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -1,11 +1,11 @@
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import * as vueSrc from 'vue';
describe('RecentSearchesRoot', () => {
describe('render', () => {
let recentSearchesRoot;
let data;
let template;
+ let VueSpy;
beforeEach(() => {
recentSearchesRoot = {
@@ -14,7 +14,7 @@ describe('RecentSearchesRoot', () => {
},
};
- spyOn(vueSrc, 'default').and.callFake((options) => {
+ VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake((options) => {
data = options.data;
template = options.template;
});
@@ -23,7 +23,7 @@ describe('RecentSearchesRoot', () => {
});
it('should instantiate Vue', () => {
- expect(vueSrc.default).toHaveBeenCalled();
+ expect(VueSpy).toHaveBeenCalled();
expect(data()).toBe(recentSearchesRoot.store.state);
expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 5393502196e..7f9c4811fba 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -1,9 +1,8 @@
/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
import $ from 'jquery';
-import '~/gl_dropdown';
+import GLDropdown from '~/gl_dropdown';
import '~/lib/utils/common_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
@@ -138,13 +137,13 @@ describe('glDropdown', function describeDropdown() {
expect(this.dropdownContainerElement).toHaveClass('open');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
- spyOn(urlUtils, 'visitUrl').and.stub();
+ const visitUrl = spyOnDependency(GLDropdown, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation);
+ if (linkedLocation && linkedLocation !== '#') expect(visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index d8428bd0e08..2b92c485f41 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import * as utils from '~/lib/utils/url_utility';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -177,7 +176,7 @@ describe('AppComponent', () => {
it('should fetch groups for provided page details and update window state', (done) => {
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
spyOn(vm, 'updateGroups').and.callThrough();
- spyOn(utils, 'mergeUrlParams').and.callThrough();
+ const mergeUrlParams = spyOnDependency(appComponent, 'mergeUrlParams').and.callThrough();
spyOn(window.history, 'replaceState');
spyOn($, 'scrollTo');
@@ -193,7 +192,7 @@ describe('AppComponent', () => {
setTimeout(() => {
expect(vm.isLoading).toBe(false);
expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
+ expect(mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
expect(window.history.replaceState).toHaveBeenCalledWith({
page: jasmine.any(String),
}, jasmine.any(String), jasmine.any(String));
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index e3c942597a3..49a139855c8 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import eventHub from '~/groups/event_hub';
@@ -135,13 +134,13 @@ describe('GroupItemComponent', () => {
const group = Object.assign({}, mockParentGroupItem);
group.childrenCount = 0;
const newVm = createComponent(group);
- spyOn(urlUtils, 'visitUrl').and.stub();
+ const visitUrl = spyOnDependency(groupItemComponent, 'visitUrl').and.stub();
spyOn(eventHub, '$emit');
newVm.onClickRowGroup(event);
setTimeout(() => {
expect(eventHub.$emit).not.toHaveBeenCalled();
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ expect(visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
done();
}, 0);
});
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js
index 1415ffb7eb3..fa104ae5bcd 100644
--- a/spec/javascripts/helpers/class_spec_helper_spec.js
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js
@@ -2,7 +2,7 @@
import './class_spec_helper';
-describe('ClassSpecHelper', () => {
+describe('ClassSpecHelper', function () {
describe('itShouldBeAStaticMethod', () => {
beforeEach(() => {
class TestClass {
diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/ide/components/file_finder/index_spec.js
new file mode 100644
index 00000000000..4f208e946d2
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/index_spec.js
@@ -0,0 +1,308 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import FindFileComponent from '~/ide/components/file_finder/index.vue';
+import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../../helpers';
+import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(FindFileComponent);
+ let vm;
+
+ beforeEach(done => {
+ setFixtures('<div id="app"></div>');
+
+ vm = mountComponentWithStore(Component, {
+ store,
+ el: '#app',
+ props: {
+ index: 0,
+ },
+ });
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('with entries', () => {
+ beforeEach(done => {
+ Vue.set(vm.$store.state.entries, 'folder', {
+ ...file('folder'),
+ path: 'folder',
+ type: 'folder',
+ });
+
+ Vue.set(vm.$store.state.entries, 'index.js', {
+ ...file('index.js'),
+ path: 'index.js',
+ type: 'blob',
+ url: '/index.jsurl',
+ });
+
+ Vue.set(vm.$store.state.entries, 'component.js', {
+ ...file('component.js'),
+ path: 'component.js',
+ type: 'blob',
+ });
+
+ setTimeout(done);
+ });
+
+ it('renders list of blobs', () => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).toContain('component.js');
+ expect(vm.$el.textContent).not.toContain('folder');
+ });
+
+ it('filters entries', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).not.toContain('component.js');
+
+ done();
+ });
+ });
+
+ it('shows clear button when searchText is not empty', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show');
+ expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
+
+ done();
+ });
+ });
+
+ it('clear button resets searchText', done => {
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clear button focues search input', done => {
+ spyOn(vm.$refs.searchInput, 'focus');
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('listShowCount', () => {
+ it('returns 1 when no filtered entries exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listShowCount).toBe(1);
+
+ done();
+ });
+ });
+
+ it('returns entries length when not filtered', () => {
+ expect(vm.listShowCount).toBe(2);
+ });
+ });
+
+ describe('listHeight', () => {
+ it('returns 55 when entries exist', () => {
+ expect(vm.listHeight).toBe(55);
+ });
+
+ it('returns 33 when entries dont exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listHeight).toBe(33);
+
+ done();
+ });
+ });
+ });
+
+ describe('filteredBlobsLength', () => {
+ it('returns length of filtered blobs', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.filteredBlobsLength).toBe(1);
+
+ done();
+ });
+ });
+ });
+
+ describe('watches', () => {
+ describe('searchText', () => {
+ it('resets focusedIndex when updated', done => {
+ vm.focusedIndex = 1;
+ vm.searchText = 'test';
+
+ vm.$nextTick(() => {
+ expect(vm.focusedIndex).toBe(0);
+
+ done();
+ });
+ });
+ });
+
+ describe('fileFindVisible', () => {
+ it('returns searchText when false', done => {
+ vm.searchText = 'test';
+ vm.$store.state.fileFindVisible = true;
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$store.state.fileFindVisible = false;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('openFile', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ spyOn(vm, 'toggleFileFinder');
+ });
+
+ it('closes file finder', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ });
+
+ it('pushes to router', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(router.push).toHaveBeenCalledWith('/project/index.jsurl');
+ });
+ });
+
+ describe('onKeyup', () => {
+ it('opens file on enter key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ENTER_KEY_CODE;
+
+ spyOn(vm, 'openFile');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']);
+
+ done();
+ });
+ });
+
+ it('closes file finder on esc key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ESC_KEY_CODE;
+
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('onKeyDown', () => {
+ let el;
+
+ beforeEach(() => {
+ el = vm.$refs.searchInput;
+ });
+
+ describe('up key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = UP_KEY_CODE;
+
+ it('resets to last index when at top', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+
+ it('minus 1 from focusedIndex', () => {
+ vm.focusedIndex = 1;
+
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+ });
+
+ describe('down key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = DOWN_KEY_CODE;
+
+ it('resets to first index when at bottom', () => {
+ vm.focusedIndex = 1;
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+
+ it('adds 1 to focusedIndex', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+ });
+ });
+ });
+
+ describe('without entries', () => {
+ it('renders loading text when loading', done => {
+ store.state.loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('Loading...');
+
+ done();
+ });
+ });
+
+ it('renders no files text', () => {
+ expect(vm.$el.textContent).toContain('No files found.');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/ide/components/file_finder/item_spec.js
new file mode 100644
index 00000000000..0f1116c6912
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/item_spec.js
@@ -0,0 +1,140 @@
+import Vue from 'vue';
+import ItemComponent from '~/ide/components/file_finder/item.vue';
+import { file } from '../../helpers';
+import createComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(ItemComponent);
+ let vm;
+ let localFile;
+
+ beforeEach(() => {
+ localFile = {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ };
+
+ vm = createComponent(Component, {
+ file: localFile,
+ focused: true,
+ searchText: '',
+ index: 0,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file name & path', () => {
+ expect(vm.$el.textContent).toContain('test file');
+ expect(vm.$el.textContent).toContain('test/file');
+ });
+
+ describe('focused', () => {
+ it('adds is-focused class', () => {
+ expect(vm.$el.classList).toContain('is-focused');
+ });
+
+ it('does not have is-focused class when not focused', done => {
+ vm.focused = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).not.toContain('is-focused');
+
+ done();
+ });
+ });
+ });
+
+ describe('changed file icon', () => {
+ it('does not render when not a changed or temp file', () => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ });
+
+ it('renders when a changed file', done => {
+ vm.file.changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders when a temp file', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ it('emits event when clicked', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ });
+
+ describe('path', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-path');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('adds ellipsis to long text', done => {
+ vm.file.path = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ done();
+ });
+ });
+ });
+
+ describe('name', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-name');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('does not add ellipsis to long text', done => {
+ vm.file.name = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 5bd890094cc..7bfcfc90572 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import Mousetrap from 'mousetrap';
import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
@@ -38,4 +39,68 @@ describe('ide component', () => {
done();
});
});
+
+ describe('file finder', () => {
+ beforeEach(done => {
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$store.state.fileFindVisible = true;
+
+ vm.$nextTick(done);
+ });
+
+ it('calls toggleFileFinder on `t` key press', done => {
+ Mousetrap.trigger('t');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `command+p` key press', done => {
+ Mousetrap.trigger('command+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `ctrl+p` key press', done => {
+ Mousetrap.trigger('ctrl+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('always allows `command+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
+ ).toBe(false);
+ });
+
+ it('always allows `ctrl+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
+ ).toBe(false);
+ });
+
+ it('onlys handles `t` when focused in input-field', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
+ ).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 22a7441ba92..b6eadf56f9d 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -1,6 +1,5 @@
-import * as urlUtils from '~/lib/utils/url_utility';
+import actions, { stageAllChanges, unstageAllChanges, toggleFileFinder } from '~/ide/stores/actions';
import store from '~/ide/stores';
-import * as actions from '~/ide/stores/actions';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
@@ -17,12 +16,12 @@ describe('Multi-file store actions', () => {
describe('redirectToUrl', () => {
it('calls visitUrl', done => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(actions, 'visitUrl');
store
.dispatch('redirectToUrl', 'test')
.then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
+ expect(visitUrl).toHaveBeenCalledWith('test');
done();
})
@@ -298,7 +297,7 @@ describe('Multi-file store actions', () => {
store.state.changedFiles.push(file(), file('new'));
testAction(
- actions.stageAllChanges,
+ stageAllChanges,
null,
store.state,
[
@@ -316,7 +315,7 @@ describe('Multi-file store actions', () => {
store.state.stagedFiles.push(file(), file('new'));
testAction(
- actions.unstageAllChanges,
+ unstageAllChanges,
null,
store.state,
[
@@ -340,4 +339,17 @@ describe('Multi-file store actions', () => {
.catch(done.fail);
});
});
+
+ describe('toggleFileFinder', () => {
+ it('commits TOGGLE_FILE_FINDER', done => {
+ testAction(
+ toggleFileFinder,
+ true,
+ null,
+ [{ type: 'TOGGLE_FILE_FINDER', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 8d04b83928c..b6b4dd28729 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -64,4 +64,24 @@ describe('IDE store getters', () => {
expect(getters.currentMergeRequest(localState)).toBeNull();
});
});
+
+ describe('allBlobs', () => {
+ beforeEach(() => {
+ Object.assign(localState.entries, {
+ index: { type: 'blob', name: 'index', lastOpenedAt: 0 },
+ app: { type: 'blob', name: 'blob', lastOpenedAt: 0 },
+ folder: { type: 'folder', name: 'folder', lastOpenedAt: 0 },
+ });
+ });
+
+ it('returns only blobs', () => {
+ expect(getters.allBlobs(localState).length).toBe(2);
+ });
+
+ it('returns list sorted by lastOpenedAt', () => {
+ localState.entries.app.lastOpenedAt = new Date().getTime();
+
+ expect(getters.allBlobs(localState)[0].name).toBe('blob');
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 116967208e0..b2b4b85ca42 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -1,7 +1,7 @@
+import actions from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
-import * as urlUtils from '~/lib/utils/url_utility';
import eventHub from '~/ide/eventhub';
import * as consts from '~/ide/stores/modules/commit/constants';
import { resetStore, file } from 'spec/ide/helpers';
@@ -307,8 +307,10 @@ describe('IDE commit module actions', () => {
});
describe('commitChanges', () => {
+ let visitUrl;
+
beforeEach(() => {
- spyOn(urlUtils, 'visitUrl');
+ visitUrl = spyOnDependency(actions, 'visitUrl');
document.body.innerHTML += '<div class="flash-container"></div>';
@@ -461,7 +463,7 @@ describe('IDE commit module actions', () => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(
+ expect(visitUrl).toHaveBeenCalledWith(
`webUrl/merge_requests/new?merge_request[source_branch]=${
store.getters['commit/newBranchName']
}&merge_request[target_branch]=master`,
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 26e7ed4535e..575039e755e 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -86,4 +86,12 @@ describe('Multi-file store mutations', () => {
expect(localState.viewer).toBe('diff');
});
});
+
+ describe('TOGGLE_FILE_FINDER', () => {
+ it('updates fileFindVisible', () => {
+ mutations.TOGGLE_FILE_FINDER(localState, true);
+
+ expect(localState.fileFindVisible).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index d5a87b5ce20..bf1f0c822fe 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -2,7 +2,6 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import '~/behaviors/markdown/render_gfm';
-import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
@@ -174,7 +173,7 @@ describe('Issuable output', () => {
});
it('does not redirect if issue has not moved', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -187,16 +186,13 @@ describe('Issuable output', () => {
vm.updateIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).not.toHaveBeenCalled();
-
+ expect(visitUrl).not.toHaveBeenCalled();
done();
});
});
it('redirects if returned web_url has changed', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -209,10 +205,7 @@ describe('Issuable output', () => {
vm.updateIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).toHaveBeenCalledWith('/testing-issue-move');
-
+ expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
done();
});
});
@@ -340,7 +333,7 @@ describe('Issuable output', () => {
describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -352,16 +345,13 @@ describe('Issuable output', () => {
vm.deleteIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).toHaveBeenCalledWith('/test');
-
+ expect(visitUrl).toHaveBeenCalledWith('/test');
done();
});
});
it('stops polling when deleting', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.poll, 'stop').and.callThrough();
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
@@ -377,7 +367,6 @@ describe('Issuable output', () => {
expect(
vm.poll.stop,
).toHaveBeenCalledWith();
-
done();
});
});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index d96151a8a3a..889c8545faa 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import descriptionComponent from '~/issue_show/components/description.vue';
-import * as taskList from '~/task_list';
+import Description from '~/issue_show/components/description.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Description component', () => {
@@ -17,7 +16,7 @@ describe('Description component', () => {
};
beforeEach(() => {
- DescriptionComponent = Vue.extend(descriptionComponent);
+ DescriptionComponent = Vue.extend(Description);
if (!document.querySelector('.issuable-meta')) {
const metaData = document.createElement('div');
@@ -82,18 +81,20 @@ describe('Description component', () => {
});
describe('TaskList', () => {
+ let TaskList;
+
beforeEach(() => {
vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
issuableType: 'issuableType',
}));
- spyOn(taskList, 'default');
+ TaskList = spyOnDependency(Description, 'TaskList');
});
it('re-inits the TaskList when description changed', (done) => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).toHaveBeenCalled();
+ expect(TaskList).toHaveBeenCalled();
done();
});
});
@@ -103,7 +104,7 @@ describe('Description component', () => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).not.toHaveBeenCalled();
+ expect(TaskList).not.toHaveBeenCalled();
done();
});
});
@@ -112,7 +113,7 @@ describe('Description component', () => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).toHaveBeenCalledWith({
+ expect(TaskList).toHaveBeenCalledWith({
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index c6bbacf237a..da00b615c9b 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility';
import Job from '~/job';
import '~/breakpoints';
@@ -22,7 +21,7 @@ describe('Job', () => {
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
- spyOn(urlUtils, 'visitUrl');
+ spyOnDependency(Job, 'visitUrl');
response = {};
diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js
index 6b397c22fb9..9c4454252ce 100644
--- a/spec/javascripts/jobs/sidebar_details_block_spec.js
+++ b/spec/javascripts/jobs/sidebar_details_block_spec.js
@@ -102,7 +102,7 @@ describe('Sidebar details block', () => {
});
it('should render runner ID', () => {
- expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: #1');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: local ci runner (#1)');
});
it('should render timeout information', () => {
diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js
index c484213df8e..81a39a97a84 100644
--- a/spec/javascripts/lib/utils/csrf_token_spec.js
+++ b/spec/javascripts/lib/utils/csrf_token_spec.js
@@ -1,6 +1,6 @@
import csrf from '~/lib/utils/csrf';
-describe('csrf', () => {
+describe('csrf', function () {
beforeEach(() => {
this.tokenKey = 'X-CSRF-Token';
this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ==';
diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js
index 75addfcc833..a7eff419fba 100644
--- a/spec/javascripts/lib/utils/image_utility_spec.js
+++ b/spec/javascripts/lib/utils/image_utility_spec.js
@@ -1,4 +1,4 @@
-import * as imageUtility from '~/lib/utils/image_utility';
+import { isImageLoaded } from '~/lib/utils/image_utility';
describe('imageUtility', () => {
describe('isImageLoaded', () => {
@@ -8,7 +8,7 @@ describe('imageUtility', () => {
naturalHeight: 100,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ expect(isImageLoaded(element)).toEqual(false);
});
it('should return false when naturalHeight = 0', () => {
@@ -17,7 +17,7 @@ describe('imageUtility', () => {
naturalHeight: 0,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ expect(isImageLoaded(element)).toEqual(false);
});
it('should return true when image.complete and naturalHeight != 0', () => {
@@ -26,7 +26,7 @@ describe('imageUtility', () => {
naturalHeight: 100,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(true);
+ expect(isImageLoaded(element)).toEqual(true);
});
});
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 79c8cf0ba32..3dbd9756cd2 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -3,7 +3,6 @@
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
@@ -356,7 +355,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -372,7 +371,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -385,7 +384,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
@@ -422,7 +421,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
@@ -439,7 +438,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -451,7 +450,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index 88aa7659275..08d54946787 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -1,7 +1,7 @@
import MonitoringStore from '~/monitoring/stores/monitoring_store';
import MonitoringMock, { deploymentData } from './mock_data';
-describe('MonitoringStore', () => {
+describe('MonitoringStore', function () {
this.store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index ec56ab0e2f0..0952356c2f4 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -3,7 +3,6 @@ import $ from 'jquery';
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
@@ -222,7 +221,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('sets target when hash matches', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(hash);
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue(hash);
Notes.updateNoteTargetSelector($note);
@@ -231,7 +230,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('unsets target when hash does not match', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist');
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue('note_doesnotexist');
Notes.updateNoteTargetSelector($note);
@@ -239,7 +238,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('unsets target when there is not a hash fragment anymore', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(null);
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue(null);
Notes.updateNoteTargetSelector($note);
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index b09494f0b77..04f2e7ef4f9 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -1,15 +1,25 @@
-/* global fixture */
+import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as utils from '~/lib/utils/url_utility';
import Pager from '~/pager';
describe('pager', () => {
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
describe('init', () => {
const originalHref = window.location.href;
beforeEach(() => {
setFixtures('<div class="content_list"></div><div class="loading"></div>');
+ spyOn($.fn, 'endlessScroll').and.stub();
});
afterEach(() => {
@@ -25,7 +35,7 @@ describe('pager', () => {
it('should use current url if data-href attribute not provided', () => {
const href = `${gl.TEST_HOST}/some_list`;
- spyOn(utils, 'removeParams').and.returnValue(href);
+ spyOnDependency(Pager, 'removeParams').and.returnValue(href);
Pager.init();
expect(Pager.url).toBe(href);
});
@@ -39,42 +49,37 @@ describe('pager', () => {
it('keeps extra query parameters from url', () => {
window.history.replaceState({}, null, '?filter=test&offset=100');
const href = `${gl.TEST_HOST}/some_list?filter=test`;
- spyOn(utils, 'removeParams').and.returnValue(href);
+ const removeParams = spyOnDependency(Pager, 'removeParams').and.returnValue(href);
Pager.init();
- expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+ expect(removeParams).toHaveBeenCalledWith(['limit', 'offset']);
expect(Pager.url).toEqual(href);
});
});
describe('getOld', () => {
const urlRegex = /(.*)some_list(.*)$/;
- let mock;
function mockSuccess() {
- mock.onGet(urlRegex).reply(200, {
+ axiosMock.onGet(urlRegex).reply(200, {
count: 0,
html: '',
});
}
function mockError() {
- mock.onGet(urlRegex).networkError();
+ axiosMock.onGet(urlRegex).networkError();
}
beforeEach(() => {
- setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+ setFixtures(
+ '<div class="content_list" data-href="/some_list"></div><div class="loading"></div>',
+ );
spyOn(axios, 'get').and.callThrough();
- mock = new MockAdapter(axios);
-
Pager.init();
});
- afterEach(() => {
- mock.restore();
- });
-
- it('shows loader while loading next page', (done) => {
+ it('shows loader while loading next page', done => {
mockSuccess();
spyOn(Pager.loading, 'show');
@@ -87,7 +92,7 @@ describe('pager', () => {
});
});
- it('hides loader on success', (done) => {
+ it('hides loader on success', done => {
mockSuccess();
spyOn(Pager.loading, 'hide');
@@ -100,7 +105,7 @@ describe('pager', () => {
});
});
- it('hides loader on error', (done) => {
+ it('hides loader on error', done => {
mockError();
spyOn(Pager.loading, 'hide');
@@ -113,7 +118,7 @@ describe('pager', () => {
});
});
- it('sends request to url with offset and limit params', (done) => {
+ it('sends request to url with offset and limit params', done => {
Pager.offset = 100;
Pager.limit = 20;
Pager.getOld();
diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index a6fe9fb65e9..b69e5f9a3a0 100644
--- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -2,7 +2,6 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
-import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -24,7 +23,7 @@ describe('stop_jobs_modal.vue', () => {
describe('onSubmit', () => {
it('stops jobs and redirects to overview page', (done) => {
const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`;
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(props.url);
return Promise.resolve({
@@ -44,7 +43,7 @@ describe('stop_jobs_modal.vue', () => {
it('displays error if stopping jobs failed', (done) => {
const dummyError = new Error('stopping jobs failed');
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(props.url);
return Promise.reject(dummyError);
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
index 6074e06fcec..94401beb5c9 100644
--- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -3,7 +3,6 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
-import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -40,7 +39,7 @@ describe('delete_milestone_modal.vue', () => {
},
});
});
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.then(() => {
@@ -60,7 +59,7 @@ describe('delete_milestone_modal.vue', () => {
eventHub.$emit.calls.reset();
return Promise.reject(dummyError);
});
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.catch((error) => {
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index f95a7cef18a..fb7d2763b49 100644
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -6,7 +6,7 @@ const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
const docsUrl = 'help/ci/scheduled_pipelines';
-describe('Pipeline Schedule Callout', () => {
+describe('Pipeline Schedule Callout', function () {
beforeEach(() => {
setFixtures(`
<div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 80770a61011..e264b16335f 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -9,8 +9,6 @@ import Sidebar from '~/right_sidebar';
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
- this.sidebar = null;
-
$aside = null;
$toggle = null;
@@ -43,7 +41,7 @@ import Sidebar from '~/right_sidebar';
beforeEach(function() {
loadFixtures(fixtureName);
mock = new MockAdapter(axios);
- this.sidebar = new Sidebar();
+ new Sidebar(); // eslint-disable-line no-new
$aside = $('.right-sidebar');
$page = $('.layout-page');
$icon = $aside.find('i');
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 1a27955983d..4f515f98a7e 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -4,7 +4,6 @@ import $ from 'jquery';
import '~/gl_dropdown';
import SearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('Search autocomplete dropdown', () => {
var assertLinks,
@@ -129,9 +128,6 @@ describe('Search autocomplete dropdown', () => {
beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw');
- // Prevent turbolinks from triggering within gl_dropdown
- spyOn(urlUtils, 'visitUrl').and.returnValue(true);
-
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
diff --git a/spec/javascripts/shortcuts_dashboard_navigation_spec.js b/spec/javascripts/shortcuts_dashboard_navigation_spec.js
index 888b49004bf..7cb201e01d8 100644
--- a/spec/javascripts/shortcuts_dashboard_navigation_spec.js
+++ b/spec/javascripts/shortcuts_dashboard_navigation_spec.js
@@ -1,24 +1,23 @@
import findAndFollowLink from '~/shortcuts_dashboard_navigation';
-import * as urlUtility from '~/lib/utils/url_utility';
describe('findAndFollowLink', () => {
it('visits a link when the selector exists', () => {
const href = '/some/path';
- const locationSpy = spyOn(urlUtility, 'visitUrl');
+ const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
findAndFollowLink('.my-shortcut');
- expect(locationSpy).toHaveBeenCalledWith(href);
+ expect(visitUrl).toHaveBeenCalledWith(href);
});
it('does not throw an exception when the selector does not exist', () => {
- const locationSpy = spyOn(urlUtility, 'visitUrl');
+ const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
// this should not throw an exception
findAndFollowLink('.this-selector-does-not-exist');
- expect(locationSpy).not.toHaveBeenCalled();
+ expect(visitUrl).not.toHaveBeenCalled();
});
});
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index b0d714cbefb..d73608ed0ed 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -4,7 +4,7 @@ import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
-describe('ShortcutsIssuable', () => {
+describe('ShortcutsIssuable', function () {
const fixtureName = 'merge_requests/diff_comment.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index afa18cc127e..da950258a94 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -1,12 +1,11 @@
import _ from 'underscore';
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
import Mock from './mock_data';
-describe('Sidebar mediator', () => {
+describe('Sidebar mediator', function() {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.mediator = new SidebarMediator(Mock.mediator);
@@ -87,12 +86,12 @@ describe('Sidebar mediator', () => {
const moveToProjectId = 7;
this.mediator.store.setMoveToProjectId(moveToProjectId);
spyOn(this.mediator.service, 'moveIssue').and.callThrough();
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl');
this.mediator.moveIssue()
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+ expect(visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
index d8e636cbdf0..a3fb965fbab 100644
--- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -7,7 +7,7 @@ import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import Mock from './mock_data';
-describe('SidebarMoveIssue', () => {
+describe('SidebarMoveIssue', function () {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.mediator = new SidebarMediator(Mock.mediator);
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index 3591f96ff87..08b112a54ba 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -31,7 +31,7 @@ const PARTICIPANT_LIST = [
{ ...PARTICIPANT, id: 3 },
];
-describe('Sidebar store', () => {
+describe('Sidebar store', function () {
beforeEach(() => {
this.store = new SidebarStore({
currentUser: {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 14bff05e537..bcd15f5eae2 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -1,4 +1,5 @@
-/* eslint-disable jasmine/no-global-setup */
+/* eslint-disable jasmine/no-global-setup, jasmine/no-unsafe-spy, no-underscore-dangle */
+
import $ from 'jquery';
import 'vendor/jasmine-jquery';
import '~/commons';
@@ -55,6 +56,17 @@ window.addEventListener('unhandledrejection', event => {
console.error(event.reason.stack || event.reason);
});
+// Add global function to spy on a module's dependencies via rewire
+window.spyOnDependency = (module, name) => {
+ const dependency = module.__GetDependency__(name);
+ const spy = jasmine.createSpy(name, dependency);
+ module.__Rewire__(name, spy);
+ return spy;
+};
+
+// Reset any rewired modules after each test (see babel-plugin-rewire)
+afterEach(__rewire_reset_all__); // eslint-disable-line
+
// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
// because it appears to lock up the thread that communicates to Karma's socket
// This async beforeEach gets called on every spec and releases the JS thread long
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index 898bbb3819b..e74f4bdef7e 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import * as urlUtils from '~/lib/utils/url_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils';
@@ -18,7 +17,7 @@ describe('Todos', () => {
it('opens the todo url', (done) => {
const todoLink = todoItem.dataset.url;
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(Todos, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(todoLink);
done();
});
@@ -33,7 +32,7 @@ describe('Todos', () => {
beforeEach(() => {
metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
- visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {});
+ visitUrlSpy = spyOnDependency(Todos, 'visitUrl').and.callFake(() => {});
windowOpenSpy = spyOn(window, 'open').and.callFake(() => {});
});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 39c47a5c06d..d84b13b07c4 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -3,7 +3,7 @@ import U2FAuthenticate from '~/u2f/authenticate';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
-describe('U2FAuthenticate', () => {
+describe('U2FAuthenticate', function () {
preloadFixtures('u2f/authenticate.html.raw');
beforeEach((done) => {
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 136b4cad737..d9383314891 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -3,7 +3,7 @@ import U2FRegister from '~/u2f/register';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
-describe('U2FRegister', () => {
+describe('U2FRegister', function () {
preloadFixtures('u2f/register.html.raw');
beforeEach((done) => {
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index ff8d54c029f..c82ba61a5b1 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { getTimeago } from '~/lib/utils/datetime_utility';
@@ -117,13 +116,13 @@ describe('Deployment component', () => {
it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
- spyOn(urlUtils, 'visitUrl').and.returnValue(true);
+ const visitUrl = spyOnDependency(deploymentComponent, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(url);
+ expect(visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 300b7882d03..81c16593eb4 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import * as simplePoll from '~/lib/utils/simple_poll';
const commitMessage = 'This is the commit message';
const commitMessageWithDescription = 'This is the commit message description';
@@ -355,9 +354,9 @@ describe('ReadyToMerge', () => {
describe('initiateMergePolling', () => {
it('should call simplePoll', () => {
- spyOn(simplePoll, 'default');
+ const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateMergePolling();
- expect(simplePoll.default).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalled();
});
});
@@ -457,11 +456,11 @@ describe('ReadyToMerge', () => {
describe('initiateRemoveSourceBranchPolling', () => {
it('should emit event and call simplePoll', () => {
spyOn(eventHub, '$emit');
- spyOn(simplePoll, 'default');
+ const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateRemoveSourceBranchPolling();
expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
- expect(simplePoll.default).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalled();
});
});
@@ -524,18 +523,20 @@ describe('ReadyToMerge', () => {
});
describe('when user can merge and can delete branch', () => {
+ let customVm;
+
beforeEach(() => {
- this.customVm = createComponent({
+ customVm = createComponent({
mr: { canRemoveSourceBranch: true },
});
});
it('isRemoveSourceBranchButtonDisabled should be false', () => {
- expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
+ expect(customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
});
it('should be enabled in rendered output', () => {
- const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input');
+ const checkboxElement = customVm.$el.querySelector('#remove-source-branch-input');
expect(checkboxElement).not.toBeNull();
});
});
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 14d055cbcc1..99872211a4e 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -62,5 +62,19 @@ describe Backup::Files do
subject.restore
end
end
+
+ describe 'folders that are a mountpoint' do
+ before do
+ allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY)
+ allow(subject).to receive(:run_pipeline!).and_return(true)
+ end
+
+ it 'shows error message' do
+ expect(subject).to receive(:resource_busy_error).with("/var/gitlab-registry")
+ .and_call_original
+
+ expect { subject.restore }.to raise_error(/is a mountpoint/)
+ end
+ end
end
end
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index e4c1c9bafc0..b3777be312b 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -81,6 +81,18 @@ describe Backup::Repository do
subject.restore
end
end
+
+ describe 'folder that is a mountpoint' do
+ before do
+ allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY)
+ end
+
+ it 'shows error message' do
+ expect(subject).to receive(:resource_busy_error).and_call_original
+
+ expect { subject.restore }.to raise_error(/is a mountpoint/)
+ end
+ end
end
describe '#empty_repo?' do
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index cab2169593a..653c19942ea 100644
--- a/spec/lib/gitlab/auth/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -25,20 +25,20 @@ describe Gitlab::Auth::LDAP::User do
OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case)
end
- describe '#changed?' do
+ describe '#should_save?' do
it "marks existing ldap user as changed" do
create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
- expect(ldap_user.changed?).to be_truthy
+ expect(ldap_user.should_save?).to be_truthy
end
it "marks existing non-ldap user if the email matches as changed" do
create(:user, email: 'john@example.com')
- expect(ldap_user.changed?).to be_truthy
+ expect(ldap_user.should_save?).to be_truthy
end
it "does not mark existing ldap user as changed" do
create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
- expect(ldap_user.changed?).to be_falsey
+ expect(ldap_user.should_save?).to be_falsey
end
end
diff --git a/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb
new file mode 100644
index 00000000000..528f1b4ec57
--- /dev/null
+++ b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::OAuth::IdentityLinker do
+ let(:user) { create(:user) }
+ let(:provider) { 'twitter' }
+ let(:uid) { user.email }
+ let(:oauth) { { 'provider' => provider, 'uid' => uid } }
+
+ subject { described_class.new(user, oauth) }
+
+ context 'linked identity exists' do
+ let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
+
+ it "doesn't create new identity" do
+ expect { subject.link }.not_to change { Identity.count }
+ end
+
+ it "sets #changed? to false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+ end
+
+ context 'identity already linked to different user' do
+ let!(:identity) { create(:identity, provider: provider, extern_uid: uid) }
+
+ it "#changed? returns false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+
+ it 'exposes error message' do
+ expect(subject.error_message).to eq 'Extern uid has already been taken'
+ end
+ end
+
+ context 'identity needs to be created' do
+ it 'creates linked identity' do
+ expect { subject.link }.to change { user.identities.count }
+ end
+
+ it 'sets identity provider' do
+ subject.link
+
+ expect(user.identities.last.provider).to eq provider
+ end
+
+ it 'sets identity extern_uid' do
+ subject.link
+
+ expect(user.identities.last.extern_uid).to eq uid
+ end
+
+ it 'sets #changed? to true' do
+ subject.link
+
+ expect(subject).to be_changed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/saml/identity_linker_spec.rb b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb
new file mode 100644
index 00000000000..f3305d574cc
--- /dev/null
+++ b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::Saml::IdentityLinker do
+ let(:user) { create(:user) }
+ let(:provider) { 'saml' }
+ let(:uid) { user.email }
+ let(:oauth) { { 'provider' => provider, 'uid' => uid } }
+
+ subject { described_class.new(user, oauth) }
+
+ context 'linked identity exists' do
+ let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
+
+ it "doesn't create new identity" do
+ expect { subject.link }.not_to change { Identity.count }
+ end
+
+ it "sets #changed? to false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+ end
+
+ context 'identity needs to be created' do
+ it 'creates linked identity' do
+ expect { subject.link }.to change { user.identities.count }
+ end
+
+ it 'sets identity provider' do
+ subject.link
+
+ expect(user.identities.last.provider).to eq provider
+ end
+
+ it 'sets identity extern_uid' do
+ subject.link
+
+ expect(user.identities.last.extern_uid).to eq uid
+ end
+
+ it 'sets #changed? to true' do
+ subject.link
+
+ expect(subject).to be_changed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f128c1d4ca4..e2bb378f663 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Play do
let(:user) { create(:user) }
- let(:project) { build.project }
- let(:build) { create(:ci_build, :manual) }
+ let(:project) { create(:project, :stubbed_repository) }
+ let(:build) { create(:ci_build, :manual, project: project) }
let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
subject { described_class.new(status) }
@@ -46,6 +46,8 @@ describe Gitlab::Ci::Status::Build::Play do
context 'when user can not push to the branch' do
before do
build.project.add_developer(user)
+ create(:protected_branch, :masters_can_push,
+ name: build.ref, project: project)
end
it { is_expected.not_to have_action }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 897a5984782..e7f20f81fe0 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -286,6 +286,7 @@ project:
- internal_ids
- project_deploy_tokens
- deploy_tokens
+- ci_cd_settings
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 2a23db5e666..f9d3116de05 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -547,3 +547,5 @@ Badge:
- created_at
- updated_at
- type
+ProjectCiCdSetting:
+- group_runners_enabled
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 40c8286b1b9..97b6069f64d 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::UserAccess do
let(:empty_project) { create(:project_empty_repo) }
let(:project_access) { described_class.new(user, project: empty_project) }
+ it 'returns true for admins' do
+ user.update!(admin: true)
+
+ expect(access.can_push_to_branch?('master')).to be_truthy
+ end
+
it 'returns true if user is master' do
empty_project.add_master(user)
@@ -71,6 +77,12 @@ describe Gitlab::UserAccess do
let(:branch) { create :protected_branch, project: project, name: "test" }
let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
+ it 'returns true for admins' do
+ user.update!(admin: true)
+
+ expect(access.can_push_to_branch?(branch.name)).to be_truthy
+ end
+
it 'returns true if user is a master' do
project.add_master(user)
diff --git a/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
new file mode 100644
index 00000000000..b8c3a3eda4e
--- /dev/null
+++ b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180425131009_assure_commits_count_for_merge_request_diff.rb')
+
+describe AssureCommitsCountForMergeRequestDiff, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are still unmigrated commit_counts afterwards' do
+ let(:namespaces) { table('namespaces') }
+ let(:projects) { table('projects') }
+ let(:merge_requests) { table('merge_requests') }
+ let(:diffs) { table('merge_request_diffs') }
+
+ before do
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+ merge_request = merge_requests.create!(source_branch: 'x', target_branch: 'y', target_project_id: project.id)
+ diffs.create!(commits_count: nil, merge_request_id: merge_request.id)
+ diffs.create!(commits_count: nil, merge_request_id: merge_request.id)
+ end
+
+ it 'migrates commit_counts sequentially in batches' do
+ migrate!
+
+ expect(migration).to have_received(:perform).once
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 56161bfcc28..25d6597084c 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :stubbed_repository) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
@@ -201,7 +201,7 @@ describe Environment do
end
describe '#stop_with_action!' do
- let(:user) { create(:admin) }
+ let(:user) { create(:user) }
subject { environment.stop_with_action!(user) }
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
new file mode 100644
index 00000000000..4aa62028169
--- /dev/null
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectCiCdSetting do
+ describe '.available?' do
+ before do
+ described_class.reset_column_information
+ end
+
+ it 'returns true' do
+ expect(described_class).to be_available
+ end
+
+ it 'memoizes the schema version' do
+ expect(ActiveRecord::Migrator)
+ .to receive(:current_version)
+ .and_call_original
+ .once
+
+ 2.times { described_class.available? }
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f00cebba364..127eb998abe 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -93,6 +93,15 @@ describe Project do
end
end
+ context 'when creating a new project' do
+ it 'automatically creates a CI/CD settings row' do
+ project = create(:project)
+
+ expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
+ expect(project.ci_cd_settings).to be_persisted
+ end
+ end
+
describe '#members & #requesters' do
let(:project) { create(:project, :public, :access_requestable) }
let(:requester) { create(:user) }
@@ -1483,52 +1492,6 @@ describe Project do
end
end
- describe '#user_can_push_to_empty_repo?' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- it 'returns false when default_branch_protection is in full protection and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
-
- it 'returns false when default_branch_protection only lets devs merge and user is dev' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
-
- it 'returns true when default_branch_protection lets devs push and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns true when default_branch_protection is unprotected and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns true when user is master' do
- project.add_master(user)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns false when the repo is not empty' do
- project.add_master(user)
- expect(project).to receive(:empty_repo?).and_return(false)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
- end
-
describe '#container_registry_url' do
let(:project) { create(:project) }
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 0a130c59037..830d2ee3b20 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -208,6 +208,17 @@ describe ProjectPresenter do
it 'returns nil if user cannot push' do
expect(presenter.new_file_anchor_data).to be_nil
end
+
+ context 'when the project is empty' do
+ let(:project) { create(:project, :empty_repo) }
+
+ # Since we protect the default branch for empty repos
+ it 'is empty for a developer' do
+ project.add_developer(user)
+
+ expect(presenter.new_file_anchor_data).to be_nil
+ end
+ end
end
describe '#readme_anchor_data' do
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 6ce75c65c8c..f1acfc48468 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -235,6 +235,8 @@ describe Ci::RetryPipelineService, '#execute' do
context 'when user is not allowed to trigger manual action' do
before do
project.add_developer(user)
+ create(:protected_branch, :masters_can_push,
+ name: pipeline.ref, project: project)
end
context 'when there is a failed manual action present' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index f793f55e51b..bd835a1fca6 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -306,6 +306,23 @@ describe QuickActions::InterpretService do
end
end
+ shared_examples 'copy_metadata command' do
+ it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do
+ source_issuable # populate the issue
+ todo_label # populate this label
+ inreview_label # populate this label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates[:add_label_ids]).to match_array([inreview_label.id, todo_label.id])
+
+ if source_issuable.milestone
+ expect(updates[:milestone_id]).to eq(source_issuable.milestone.id)
+ else
+ expect(updates).not_to have_key(:milestone_id)
+ end
+ end
+ end
+
shared_examples 'shrug command' do
it 'appends ¯\_(ツ)_/¯ to the comment' do
new_content, _ = service.execute(content, issuable)
@@ -757,6 +774,65 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ context '/copy_metadata command' do
+ let(:todo_label) { create(:label, project: project, title: 'To Do') }
+ let(:inreview_label) { create(:label, project: project, title: 'In Review') }
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/copy_metadata' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference}" }
+ let(:issuable) { issue }
+ end
+
+ context 'when the parent issuable has a milestone' do
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [todo_label, inreview_label], milestone: milestone) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'when more than one issuable is passed' do
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
+ let(:other_label) { create(:label, project: project, title: 'Other') }
+ let(:other_source_issuable) { create(:labeled_issue, project: project, labels: [other_label]) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference} #{other_source_issuable.to_reference}" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'cross project references' do
+ it_behaves_like 'empty command' do
+ let(:other_project) { create(:project, :public) }
+ let(:source_issuable) { create(:labeled_issue, project: other_project, labels: [todo_label, inreview_label]) }
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/copy_metadata imaginary#1234" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:other_project) { create(:project, :private) }
+ let(:source_issuable) { create(:issue, project: other_project) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+ end
+ end
+
context '/duplicate command' do
it_behaves_like 'duplicate command' do
let(:issue_duplicate) { create(:issue, project: project) }
diff --git a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
new file mode 100644
index 00000000000..72912ffb89d
--- /dev/null
+++ b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+shared_context 'Ldap::OmniauthCallbacksController' do
+ include LoginHelpers
+ include LdapHelpers
+
+ let(:uid) { 'my-uid' }
+ let(:provider) { 'ldapmain' }
+ let(:valid_login?) { true }
+ let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider) }
+ let(:ldap_server_config) do
+ { main: ldap_config_defaults(:main) }
+ end
+
+ def ldap_config_defaults(key, hash = {})
+ {
+ provider_name: "ldap#{key}",
+ attributes: {},
+ encryption: 'plain'
+ }.merge(hash)
+ end
+
+ before do
+ stub_ldap_setting(enabled: true, servers: ldap_server_config)
+ described_class.define_providers!
+ Rails.application.reload_routes!
+
+ mock_auth_hash(provider.to_s, uid, user.email)
+ stub_omniauth_provider(provider, context: request)
+
+ allow(Gitlab::Auth::LDAP::Access).to receive(:allowed?).and_return(valid_login?)
+ end
+end
diff --git a/spec/support/helpers/ldap_helpers.rb b/spec/support/helpers/ldap_helpers.rb
index 0e87b3d359d..b90bbc4b106 100644
--- a/spec/support/helpers/ldap_helpers.rb
+++ b/spec/support/helpers/ldap_helpers.rb
@@ -18,6 +18,10 @@ module LdapHelpers
allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages)
end
+ def stub_ldap_setting(messages)
+ allow(Gitlab.config.ldap).to receive_messages(to_settings(messages))
+ end
+
# Stub an LDAP person search and provide the return entry. Specify `nil` for
# `entry` to simulate when an LDAP person is not found
#
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index db34090e971..72e5c2d66dd 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -112,7 +112,7 @@ module LoginHelpers
}
}
})
- Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml]
+ Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
def mock_saml_config
@@ -129,7 +129,7 @@ module LoginHelpers
env = env_from_context(context)
set_devise_mapping(context: context)
- env['omniauth.auth'] = OmniAuth.config.mock_auth[provider]
+ env['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
def stub_omniauth_saml_config(messages)
diff --git a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
index e61983c60b4..f4bc6f8efa5 100644
--- a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
@@ -1,25 +1,30 @@
shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
- before do
- @issuable_ids = []
-
- %w[fix improve/awesome].each do |source_branch|
- issuable =
- if issuable_type == :issue
- create(issuable_type, project: project, author: project.creator)
- else
- create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator)
- end
-
- @issuable_ids << issuable.id
- end
- end
+ include ProjectForksHelper
- it "creates indexed meta-data object for issuable notes and votes count" do
+ def get_action(action, project)
if action
get action, author_id: project.creator.id
else
get :index, namespace_id: project.namespace, project_id: project
end
+ end
+
+ def create_issuable(issuable_type, project, source_branch:)
+ if issuable_type == :issue
+ create(issuable_type, project: project, author: project.creator)
+ else
+ create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator)
+ end
+ end
+
+ before do
+ @issuable_ids = %w[fix improve/awesome].map do |source_branch|
+ create_issuable(issuable_type, project, source_branch: source_branch).id
+ end
+ end
+
+ it "creates indexed meta-data object for issuable notes and votes count" do
+ get_action(action, project)
meta_data = assigns(:issuable_meta_data)
@@ -29,18 +34,29 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
end
end
+ it "avoids N+1 queries" do
+ control = ActiveRecord::QueryRecorder.new { get_action(action, project) }
+ issuable = create_issuable(issuable_type, project, source_branch: 'csv')
+
+ if issuable_type == :merge_request
+ issuable.update!(source_project: fork_project(project))
+ end
+
+ expect { get_action(action, project) }.not_to exceed_query_limit(control.count)
+ end
+
describe "when given empty collection" do
let(:project2) { create(:project, :public) }
it "doesn't execute any queries with false conditions" do
- get_action =
+ get_empty =
if action
proc { get action, author_id: project.creator.id }
else
proc { get :index, namespace_id: project2.namespace, project_id: project2 }
end
- expect(&get_action).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
+ expect(&get_empty).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
end
end
end
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index dcf5e4a0416..06093deb459 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index d4856090ed9..85cc1b6bb78 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 6ee7e76f676..e98d3ce7b8f 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index f05278cfde5..7aca6b0d427 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -670,6 +670,10 @@ babel-plugin-istanbul@^4.1.5:
istanbul-lib-instrument "^1.7.5"
test-exclude "^4.1.1"
+babel-plugin-rewire@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.1.0.tgz#a6b966d9d8c06c03d95dcda2eec4e2521519549b"
+
babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
@@ -8474,6 +8478,10 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
+vue-virtual-scroll-list@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a"
+
vue@^2.5.13:
version "2.5.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.13.tgz#95bd31e20efcf7a7f39239c9aa6787ce8cf578e1"