diff options
38 files changed, 718 insertions, 625 deletions
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 9f016e0338f..257a7432c20 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,6 +1,7 @@ <script> import Mousetrap from 'mousetrap'; import { mapActions, mapState, mapGetters } from 'vuex'; +import NewModal from './new_dropdown/modal.vue'; import IdeSidebar from './ide_side_bar.vue'; import RepoTabs from './repo_tabs.vue'; import IdeStatusBar from './ide_status_bar.vue'; @@ -13,6 +14,7 @@ const originalStopCallback = Mousetrap.stopCallback; export default { components: { + NewModal, IdeSidebar, RepoTabs, IdeStatusBar, @@ -137,5 +139,6 @@ export default { /> </div> <ide-status-bar :file="activeFile"/> + <new-modal /> </article> </template> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 8fc4ebe6ca6..0a95c0bb30d 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -1,12 +1,16 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import NewDropdown from './new_dropdown/index.vue'; +import Icon from '~/vue_shared/components/icon.vue'; import IdeTreeList from './ide_tree_list.vue'; +import Upload from './new_dropdown/upload.vue'; +import NewEntryButton from './new_dropdown/button.vue'; export default { components: { - NewDropdown, + Icon, + Upload, IdeTreeList, + NewEntryButton, }, computed: { ...mapState(['currentBranchId']), @@ -20,23 +24,42 @@ export default { } }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']), }, }; </script> <template> <ide-tree-list + header-class="d-flex w-100" viewer-type="editor" > <template slot="header" > {{ __('Edit') }} - <new-dropdown - :project-id="currentProject.name_with_namespace" - :branch="currentBranchId" - /> + <div class="ml-auto d-flex"> + <new-entry-button + :label="__('New file')" + :show-label="false" + class="d-flex border-0 p-0 mr-3" + icon="doc-new" + @click="openNewEntryModal({ type: 'blob' })" + /> + <upload + :show-label="false" + class="d-flex mr-3" + button-css-classes="border-0 p-0" + @create="createTempEntry" + /> + <new-entry-button + :label="__('New directory')" + :show-label="false" + class="d-flex border-0 p-0" + icon="folder-new" + @click="openNewEntryModal({ type: 'tree' })" + /> + </div> </template> </ide-tree-list> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue new file mode 100644 index 00000000000..7682b34ce4d --- /dev/null +++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue @@ -0,0 +1,51 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + label: { + type: String, + required: false, + default: null, + }, + icon: { + type: String, + required: true, + }, + iconClasses: { + type: String, + required: false, + default: null, + }, + showLabel: { + type: Boolean, + required: false, + default: true, + }, + }, + methods: { + clicked() { + this.$emit('click'); + }, + }, +}; +</script> + +<template> + <button + :aria-label="label" + type="button" + @click.stop.prevent="clicked" + > + <icon + :name="icon" + :css-classes="iconClasses" + /> + <template v-if="showLabel"> + {{ label }} + </template> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 1e398d7e1aa..c29e49ba766 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -3,12 +3,14 @@ import { mapActions } from 'vuex'; import icon from '~/vue_shared/components/icon.vue'; import newModal from './modal.vue'; import upload from './upload.vue'; +import ItemButton from './button.vue'; export default { components: { icon, newModal, upload, + ItemButton, }, props: { branch: { @@ -20,11 +22,13 @@ export default { required: false, default: '', }, + mouseOver: { + type: Boolean, + required: true, + }, }, data() { return { - openModal: false, - modalType: '', dropdownOpen: false, }; }, @@ -34,17 +38,18 @@ export default { this.$refs.dropdownMenu.scrollIntoView(); }); }, + mouseOver() { + if (!this.mouseOver) { + this.dropdownOpen = false; + } + }, }, methods: { - ...mapActions(['createTempEntry']), + ...mapActions(['createTempEntry', 'openNewEntryModal']), createNewItem(type) { - this.modalType = type; - this.openModal = true; + this.openNewEntryModal({ type, path: this.path }); this.dropdownOpen = false; }, - hideModal() { - this.openModal = false; - }, openDropdown() { this.dropdownOpen = !this.dropdownOpen; }, @@ -58,23 +63,19 @@ export default { :class="{ show: dropdownOpen, }" - class="dropdown" + class="dropdown d-flex" > <button + :aria-label="__('Create new file or directory')" type="button" - class="btn btn-sm btn-default dropdown-toggle add-to-tree" - aria-label="Create new file or directory" + class="rounded border-0 d-flex ide-entry-dropdown-toggle" @click.stop="openDropdown()" > <icon - :size="12" - name="plus" - css-classes="float-left" + name="hamburger" /> <icon - :size="12" name="arrow-down" - css-classes="float-left" /> </button> <ul @@ -82,39 +83,30 @@ export default { class="dropdown-menu dropdown-menu-right" > <li> - <a - href="#" - role="button" - @click.stop.prevent="createNewItem('blob')" - > - {{ __('New file') }} - </a> + <item-button + :label="__('New file')" + class="d-flex" + icon="doc-new" + icon-classes="mr-2" + @click="createNewItem('blob')" + /> </li> <li> <upload - :branch-id="branch" :path="path" @create="createTempEntry" /> </li> <li> - <a - href="#" - role="button" - @click.stop.prevent="createNewItem('tree')" - > - {{ __('New directory') }} - </a> + <item-button + :label="__('New directory')" + class="d-flex" + icon="folder-new" + icon-classes="mr-2" + @click="createNewItem('tree')" + /> </li> </ul> </div> - <new-modal - v-if="openModal" - :type="modalType" - :branch-id="branch" - :path="path" - @hide="hideModal" - @create="createTempEntry" - /> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 1e9668d5154..1867b7980d2 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,78 +1,70 @@ <script> import { __ } from '~/locale'; -import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { mapActions, mapState } from 'vuex'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; export default { components: { - DeprecatedModal, - }, - props: { - branchId: { - type: String, - required: true, - }, - type: { - type: String, - required: true, - }, - path: { - type: String, - required: true, - }, + GlModal, }, data() { return { - entryName: this.path !== '' ? `${this.path}/` : '', + name: '', }; }, computed: { + ...mapState(['newEntryModal']), + entryName: { + get() { + return this.name || (this.newEntryModal.path !== '' ? `${this.newEntryModal.path}/` : ''); + }, + set(val) { + this.name = val; + }, + }, modalTitle() { - if (this.type === 'tree') { + if (this.newEntryModal.type === 'tree') { return __('Create new directory'); } return __('Create new file'); }, buttonLabel() { - if (this.type === 'tree') { + if (this.newEntryModal.type === 'tree') { return __('Create directory'); } return __('Create file'); }, }, - mounted() { - this.$refs.fieldName.focus(); - }, methods: { + ...mapActions(['createTempEntry']), createEntryInStore() { - this.$emit('create', { - branchId: this.branchId, - name: this.entryName, - type: this.type, + this.createTempEntry({ + name: this.name, + type: this.newEntryModal.type, }); - - this.hideModal(); }, - hideModal() { - this.$emit('hide'); + focusInput() { + setTimeout(() => { + this.$refs.fieldName.focus(); + }); }, }, }; </script> <template> - <deprecated-modal - :title="modalTitle" - :primary-button-label="buttonLabel" - kind="success" - @cancel="hideModal" + <gl-modal + id="ide-new-entry" + :header-title-text="modalTitle" + :footer-primary-button-text="buttonLabel" + footer-primary-button-variant="success" @submit="createEntryInStore" + @open="focusInput" > - <form - slot="body" + <div class="form-group row" - @submit.prevent="createEntryInStore" > <label class="label-light col-form-label col-sm-3"> {{ __('Name') }} @@ -85,6 +77,6 @@ export default { class="form-control" /> </div> - </form> - </deprecated-modal> + </div> + </gl-modal> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 677b282bd61..5b1743bb30e 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -1,71 +1,85 @@ <script> - export default { - props: { - branchId: { - type: String, - required: true, - }, - path: { - type: String, - required: false, - default: '', - }, +import Icon from '~/vue_shared/components/icon.vue'; +import ItemButton from './button.vue'; + +export default { + components: { + Icon, + ItemButton, + }, + props: { + path: { + type: String, + required: false, + default: '', }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); + showLabel: { + type: Boolean, + required: false, + default: true, }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); + buttonCssClasses: { + type: String, + required: false, + default: null, }, - methods: { - createFile(target, file, isText) { - const { name } = file; - let { result } = target; + }, + mounted() { + this.$refs.fileUpload.addEventListener('change', this.openFile); + }, + beforeDestroy() { + this.$refs.fileUpload.removeEventListener('change', this.openFile); + }, + methods: { + createFile(target, file, isText) { + const { name } = file; + let { result } = target; - if (!isText) { - // eslint-disable-next-line prefer-destructuring - result = result.split('base64,')[1]; - } + if (!isText) { + // eslint-disable-next-line prefer-destructuring + result = result.split('base64,')[1]; + } - this.$emit('create', { - name: `${(this.path ? `${this.path}/` : '')}${name}`, - branchId: this.branchId, - type: 'blob', - content: result, - base64: !isText, - }); - }, - readFile(file) { - const reader = new FileReader(); - const isText = file.type.match(/text.*/) !== null; + this.$emit('create', { + name: `${this.path ? `${this.path}/` : ''}${name}`, + type: 'blob', + content: result, + base64: !isText, + }); + }, + readFile(file) { + const reader = new FileReader(); + const isText = file.type.match(/text.*/) !== null; - reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true }); + reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true }); - if (isText) { - reader.readAsText(file); - } else { - reader.readAsDataURL(file); - } - }, - openFile() { - Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); - }, - startFileUpload() { - this.$refs.fileUpload.click(); - }, + if (isText) { + reader.readAsText(file); + } else { + reader.readAsDataURL(file); + } + }, + openFile() { + Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); }, - }; + startFileUpload() { + this.$refs.fileUpload.click(); + }, + }, +}; </script> <template> <div> - <a - href="#" - role="button" - @click.stop.prevent="startFileUpload" - > - {{ __('Upload file') }} - </a> + <item-button + :class="buttonCssClasses" + :show-label="showLabel" + :icon-classes="showLabel ? 'mr-2' : ''" + :label="__('Upload file')" + class="d-flex" + icon="upload" + @click="startFileUpload" + /> <input id="file-upload" ref="fileUpload" diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index f490a3a2a39..3b4dd5ae9aa 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -40,6 +40,11 @@ export default { default: false, }, }, + data() { + return { + mouseOver: false, + }; + }, computed: { ...mapGetters([ 'getChangesInFolder', @@ -142,6 +147,9 @@ export default { hasUrlAtCurrentRoute() { return this.$router.currentRoute.path === `/project${this.file.url}`; }, + toggleHover(over) { + this.mouseOver = over; + }, }, }; </script> @@ -153,6 +161,8 @@ export default { class="file" role="button" @click="clickFile" + @mouseover="toggleHover(true)" + @mouseout="toggleHover(false)" > <div class="file-name" @@ -206,6 +216,7 @@ export default { :project-id="file.projectId" :branch="file.branchId" :path="file.path" + :mouse-over="mouseOver" class="float-right prepend-left-8" /> </div> diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 5e91fa915ff..b5bd6f5a6bb 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -52,7 +52,7 @@ export const setResizingStatus = ({ commit }, resizing) => { export const createTempEntry = ( { state, commit, dispatch }, - { branchId, name, type, content = '', base64 = false }, + { name, type, content = '', base64 = false }, ) => new Promise(resolve => { const worker = new FilesDecoratorWorker(); @@ -81,7 +81,7 @@ export const createTempEntry = ( commit(types.CREATE_TMP_ENTRY, { data, projectId: state.currentProjectId, - branchId, + branchId: state.currentBranchId, }); if (type === 'blob') { @@ -100,7 +100,7 @@ export const createTempEntry = ( worker.postMessage({ data: [fullName], projectId: state.currentProjectId, - branchId, + branchId: state.currentBranchId, type, tempFile: true, base64, @@ -178,6 +178,13 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); export const setErrorMessage = ({ commit }, errorMessage) => commit(types.SET_ERROR_MESSAGE, errorMessage); +export const openNewEntryModal = ({ commit }, { type, path = '' }) => { + commit(types.OPEN_NEW_ENTRY_MODAL, { type, path }); + + // open the modal manually so we don't mess around with dropdown/rows + $('#ide-new-entry').modal('show'); +}; + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 555802e1811..8d6f9ccaf34 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -74,3 +74,5 @@ export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; + +export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 702be2140e2..f8091f5b5e0 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -166,6 +166,11 @@ export default { [types.SET_ERROR_MESSAGE](state, errorMessage) { Object.assign(state, { errorMessage }); }, + [types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) { + Object.assign(state, { + newEntryModal: { type, path }, + }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index be229b2c723..0f32a267469 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -26,4 +26,8 @@ export default () => ({ rightPane: null, links: {}, errorMessage: null, + newEntryModal: { + type: '', + path: '', + }, }); diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index b298b989203..416eda796a7 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -45,6 +45,11 @@ export default { emitSubmit(event) { this.$emit('submit', event); }, + opened({ propertyName }) { + if (propertyName === 'opacity') { + this.$emit('open'); + } + }, }, }; </script> @@ -55,6 +60,7 @@ export default { class="modal fade" tabindex="-1" role="dialog" + @transitionend="opened" > <div :class="modalSizeClass" diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6e2b285285a..8b1227b9131 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -44,6 +44,7 @@ padding-bottom: $grid-size; .file { + height: 32px; cursor: pointer; &.file-active { @@ -716,32 +717,6 @@ justify-content: center; } -.ide-new-btn { - .btn { - padding-top: 3px; - padding-bottom: 3px; - } - - .dropdown { - display: flex; - } - - .dropdown-toggle svg { - top: 0; - } - - .dropdown-menu { - left: auto; - right: 0; - - label { - font-weight: $gl-font-weight-normal; - padding: 5px 8px; - margin-bottom: 0; - } - } -} - .ide { overflow: hidden; @@ -1340,3 +1315,24 @@ overflow: auto; } } + +.ide-entry-dropdown-toggle { + padding: $gl-padding-4; + background-color: $theme-gray-100; + + &:hover { + background-color: $theme-gray-200; + } + + &:active, + &:focus { + color: $white-normal; + background-color: $blue-500; + outline: 0; + } +} + +.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle { + color: $white-normal; + background-color: $blue-500; +} diff --git a/changelogs/unreleased/46930-fix-updated_at-if-created_at-is-set-note-api.yml b/changelogs/unreleased/46930-fix-updated_at-if-created_at-is-set-note-api.yml new file mode 100644 index 00000000000..d95714a5267 --- /dev/null +++ b/changelogs/unreleased/46930-fix-updated_at-if-created_at-is-set-note-api.yml @@ -0,0 +1,5 @@ +--- +title: Fix updated_at if created_at is set for Note API +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/ide-row-dropdown-design-update.yml b/changelogs/unreleased/ide-row-dropdown-design-update.yml new file mode 100644 index 00000000000..e0fe64c944e --- /dev/null +++ b/changelogs/unreleased/ide-row-dropdown-design-update.yml @@ -0,0 +1,5 @@ +--- +title: Updated design of new entry dropdown in Web IDE +merge_request: 20526 +author: +type: changed diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile index 76ebe57cf8d..2424e650d07 100644 --- a/danger/changelog/Dangerfile +++ b/danger/changelog/Dangerfile @@ -59,7 +59,7 @@ end if changelog_needed if changelog_found - check_changelog(path) + check_changelog(changelog_found) else warn format(MISSING_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], labels: presented_no_changelog_labels) end diff --git a/doc/api/notes.md b/doc/api/notes.md index d29c5b94915..c271d46688f 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -218,6 +218,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `snippet_id` (required) - The ID of a snippet - `body` (required) - The content of a note +- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note @@ -340,6 +341,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `merge_request_iid` (required) - The IID of a merge request - `body` (required) - The content of a note +- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z ### Modify existing merge request note diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index b4bfb677d72..e2984b08eca 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -97,6 +97,8 @@ module API current_user.admin? || parent.owned_by?(current_user) end + opts[:updated_at] = opts[:created_at] if opts[:created_at] + project = parent if parent.is_a?(Project) ::Notes::CreateService.new(project, current_user, opts).execute end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index a1a050647b9..d50ac2707f0 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -86,9 +86,6 @@ module Gitlab # Relative path of repo attr_reader :relative_path - # Rugged repo object - attr_reader :rugged - attr_reader :gitlab_projects, :storage, :gl_repository, :relative_path # This initializer method is only used on the client side (gitlab-ce). @@ -112,8 +109,9 @@ module Gitlab [storage, relative_path] == [other.storage, other.relative_path] end + # This method will be removed when Gitaly reaches v1.1. def path - @path ||= File.join( + File.join( Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path ) end @@ -127,8 +125,9 @@ module Gitlab raise Gitlab::Git::CommandError.new(e.message) end + # This method will be removed when Gitaly reaches v1.1. def rugged - @rugged ||= circuit_breaker.perform do + circuit_breaker.perform do Rugged::Repository.new(path, alternates: alternate_object_directories) end rescue Rugged::RepositoryError, Rugged::OSError @@ -713,12 +712,6 @@ module Gitlab Gitlab::Git.committer_hash(email: user.email, name: user.name) end - def create_commit(params = {}) - params[:message].delete!("\r") - - Rugged::Commit.create(rugged, params) - end - # Delete the specified branch from the repository def delete_branch(branch_name) gitaly_migrate(:delete_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| @@ -1758,6 +1751,12 @@ module Gitlab def sha_from_ref(ref) rev_parse_target(ref).oid end + + def create_commit(params = {}) + params[:message].delete!("\r") + + Rugged::Commit.create(rugged, params) + end end end end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index 5fdad077eea..2ba68343aa5 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -12,35 +12,12 @@ module Gitlab end # This method returns an array of new commit references - def new_refs - repository.rev_list(including: newrev, excluding: :all).split("\n") - end - - # Finds newly added objects - # Returns an array of shas + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233 # - # Can skip objects which do not have a path using required_path: true - # This skips commit objects and root trees, which might not be needed when - # looking for blobs - # - # When given a block it will yield objects as a lazy enumerator so - # the caller can limit work done instead of processing megabytes of data - def new_objects(options: [], require_path: nil, not_in: nil, &lazy_block) - opts = { - including: newrev, - options: options, - excluding: not_in.nil? ? :all : not_in, - require_path: require_path - } - - get_objects(opts, &lazy_block) - end - - def all_objects(options: [], require_path: nil, &lazy_block) - get_objects(including: :all, - options: options, - require_path: require_path, - &lazy_block) + def new_refs + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rev_list(including: newrev, excluding: :all).split("\n") + end end private diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 83ff735580e..0c2541b17a7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1720,6 +1720,9 @@ msgstr "" msgid "Create new file" msgstr "" +msgid "Create new file or directory" +msgstr "" + msgid "Create new label" msgstr "" diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 057b49cc68c..d3aa4912099 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -22,9 +22,7 @@ describe 'Multi-file editor new directory', :js do end it 'creates directory in current directory' do - find('.add-to-tree').click - - click_link('New directory') + all('.ide-tree-header button').last.click page.within('.modal') do find('.form-control').set('folder name') @@ -32,9 +30,7 @@ describe 'Multi-file editor new directory', :js do click_button('Create directory') end - find('.add-to-tree').click - - click_link('New file') + first('.ide-tree-header button').click page.within('.modal-dialog') do find('.form-control').set('file name') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index b324ab01383..f836783cbff 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -22,9 +22,7 @@ describe 'Multi-file editor new file', :js do end it 'creates file in current directory' do - find('.add-to-tree').click - - click_link('New file') + first('.ide-tree-header button').click page.within('.modal') do find('.form-control').set('file name') diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb index 28da0a87f22..dcf7d314f8e 100644 --- a/spec/features/projects/tree/upload_file_spec.rb +++ b/spec/features/projects/tree/upload_file_spec.rb @@ -24,14 +24,10 @@ describe 'Multi-file editor upload file', :js do end it 'uploads text file' do - find('.add-to-tree').click - # make the field visible so capybara can use it execute_script('document.querySelector("#file-upload").classList.remove("hidden")') attach_file('file-upload', txt_file) - find('.add-to-tree').click - expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt') expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline)) end diff --git a/spec/javascripts/ide/components/new_dropdown/button_spec.js b/spec/javascripts/ide/components/new_dropdown/button_spec.js new file mode 100644 index 00000000000..ef083d06ba7 --- /dev/null +++ b/spec/javascripts/ide/components/new_dropdown/button_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import Button from '~/ide/components/new_dropdown/button.vue'; + +describe('IDE new entry dropdown button component', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(Button); + }); + + beforeEach(() => { + vm = mountComponent(Component, { + label: 'Testing', + icon: 'doc-new', + }); + + spyOn(vm, '$emit'); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders button with label', () => { + expect(vm.$el.textContent).toContain('Testing'); + }); + + it('renders icon', () => { + expect(vm.$el.querySelector('.ic-doc-new')).not.toBe(null); + }); + + it('emits click event', () => { + vm.$el.click(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it('hides label if showLabel is false', done => { + vm.showLabel = false; + + vm.$nextTick(() => { + expect(vm.$el.textContent).not.toContain('Testing'); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js index 7b637f37eba..4d704b80209 100644 --- a/spec/javascripts/ide/components/new_dropdown/index_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js @@ -13,6 +13,7 @@ describe('new dropdown component', () => { vm = createComponentWithStore(component, store, { branch: 'master', path: '', + mouseOver: false, }); vm.$store.state.currentProjectId = 'abcproject'; @@ -21,6 +22,8 @@ describe('new dropdown component', () => { tree: [], }; + spyOn(vm, 'openNewEntryModal'); + vm.$mount(); }); @@ -31,50 +34,23 @@ describe('new dropdown component', () => { }); it('renders new file, upload and new directory links', () => { - expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file'); - expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file'); - expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory'); + const buttons = vm.$el.querySelectorAll('.dropdown-menu button'); + expect(buttons[0].textContent.trim()).toBe('New file'); + expect(buttons[1].textContent.trim()).toBe('Upload file'); + expect(buttons[2].textContent.trim()).toBe('New directory'); }); describe('createNewItem', () => { it('sets modalType to blob when new file is clicked', () => { - vm.$el.querySelectorAll('a')[0].click(); + vm.$el.querySelectorAll('.dropdown-menu button')[0].click(); - expect(vm.modalType).toBe('blob'); + expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'blob', path: '' }); }); it('sets modalType to tree when new directory is clicked', () => { - vm.$el.querySelectorAll('a')[2].click(); - - expect(vm.modalType).toBe('tree'); - }); - - it('opens modal when link is clicked', done => { - vm.$el.querySelectorAll('a')[0].click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - - done(); - }); - }); - }); - - describe('hideModal', () => { - beforeAll(done => { - vm.openModal = true; - Vue.nextTick(done); - }); - - it('closes modal after toggling', done => { - vm.hideModal(); + vm.$el.querySelectorAll('.dropdown-menu button')[2].click(); - Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.modal')).toBeNull(); - }) - .then(done) - .catch(done.fail); + expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'tree', path: '' }); }); }); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index f362ed4db65..6dcc5880677 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; +import { createStore } from '~/ide/stores'; import modal from '~/ide/components/new_dropdown/modal.vue'; -import createComponent from 'spec/helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; describe('new file modal component', () => { const Component = Vue.extend(modal); @@ -13,13 +14,15 @@ describe('new file modal component', () => { ['tree', 'blob'].forEach(type => { describe(type, () => { beforeEach(() => { - vm = createComponent(Component, { + const store = createStore(); + store.state.newEntryModal = { type, - branchId: 'master', path: '', - }); + }; + + vm = createComponentWithStore(Component, store).$mount(); - vm.entryName = 'testing'; + vm.name = 'testing'; }); it(`sets modal title as ${type}`, () => { @@ -40,12 +43,11 @@ describe('new file modal component', () => { describe('createEntryInStore', () => { it('$emits create', () => { - spyOn(vm, '$emit'); + spyOn(vm, 'createTempEntry'); vm.createEntryInStore(); - expect(vm.$emit).toHaveBeenCalledWith('create', { - branchId: 'master', + expect(vm.createTempEntry).toHaveBeenCalledWith({ name: 'testing', type, }); @@ -53,22 +55,4 @@ describe('new file modal component', () => { }); }); }); - - it('focuses field on mount', () => { - document.body.innerHTML += '<div class="js-test"></div>'; - - vm = createComponent( - Component, - { - type: 'tree', - branchId: 'master', - path: '', - }, - '.js-test', - ); - - expect(document.activeElement).toBe(vm.$refs.fieldName); - - vm.$el.remove(); - }); }); diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js index 2bc5d701601..9c76500cfe5 100644 --- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js @@ -9,7 +9,6 @@ describe('new dropdown upload', () => { const Component = Vue.extend(upload); vm = createComponent(Component, { - branchId: 'master', path: '', }); @@ -65,7 +64,6 @@ describe('new dropdown upload', () => { expect(vm.$emit).toHaveBeenCalledWith('create', { name: file.name, - branchId: 'master', type: 'blob', content: target.result, base64: false, @@ -77,7 +75,6 @@ describe('new dropdown upload', () => { expect(vm.$emit).toHaveBeenCalledWith('create', { name: file.name, - branchId: 'master', type: 'blob', content: binaryTarget.result.split('base64,')[1], base64: true, diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js deleted file mode 100644 index 5add150f874..00000000000 --- a/spec/javascripts/issuable_time_tracker_spec.js +++ /dev/null @@ -1,201 +0,0 @@ -/* eslint-disable no-unused-vars, func-call-spacing, no-spaced-func, semi, quotes, space-infix-ops, max-len */ - -import $ from 'jquery'; -import Vue from 'vue'; - -import timeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; - -function initTimeTrackingComponent(opts) { - setFixtures(` - <div> - <div id="mock-container"></div> - </div> - `); - - this.initialData = { - time_estimate: opts.timeEstimate, - time_spent: opts.timeSpent, - human_time_estimate: opts.timeEstimateHumanReadable, - human_time_spent: opts.timeSpentHumanReadable, - rootPath: '/', - }; - - const TimeTrackingComponent = Vue.extend(timeTracker); - this.timeTracker = new TimeTrackingComponent({ - el: '#mock-container', - propsData: this.initialData, - }); -} - -describe('Issuable Time Tracker', function() { - describe('Initialization', function() { - beforeEach(function() { - initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); - }); - - it('should return something defined', function() { - expect(this.timeTracker).toBeDefined(); - }); - - it ('should correctly set timeEstimate', function(done) { - Vue.nextTick(() => { - expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate); - done(); - }); - }); - it ('should correctly set time_spent', function(done) { - Vue.nextTick(() => { - expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent); - done(); - }); - }); - }); - - describe('Content Display', function() { - describe('Panes', function() { - describe('Comparison pane', function() { - beforeEach(function() { - initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' }); - }); - - it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) { - Vue.nextTick(() => { - const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane'); - expect(this.timeTracker.showComparisonState).toBe(true); - done(); - }); - }); - - describe('Remaining meter', function() { - it('should display the remaining meter with the correct width', function(done) { - Vue.nextTick(() => { - const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width; - const correctWidth = '5%'; - - expect(meterWidth).toBe(correctWidth); - done(); - }) - }); - - it('should display the remaining meter with the correct background color when within estimate', function(done) { - Vue.nextTick(() => { - const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill'); - expect(styledMeter.length).toBe(1); - done() - }); - }); - - it('should display the remaining meter with the correct background color when over estimate', function(done) { - this.timeTracker.time_estimate = 100000; - this.timeTracker.time_spent = 20000000; - Vue.nextTick(() => { - const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill'); - expect(styledMeter.length).toBe(1); - done(); - }); - }); - }); - }); - - describe("Estimate only pane", function() { - beforeEach(function() { - initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' }); - }); - - it('should display the human readable version of time estimated', function(done) { - Vue.nextTick(() => { - const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText; - const correctText = 'Estimated: 2h 46m'; - - expect(estimateText).toBe(correctText); - done(); - }); - }); - }); - - describe('Spent only pane', function() { - beforeEach(function() { - initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); - }); - - it('should display the human readable version of time spent', function(done) { - Vue.nextTick(() => { - const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText; - const correctText = 'Spent: 1h 23m'; - - expect(spentText).toBe(correctText); - done(); - }); - }); - }); - - describe('No time tracking pane', function() { - beforeEach(function() { - initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' }); - }); - - it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) { - Vue.nextTick(() => { - const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane'); - const noTrackingText =$noTrackingPane.innerText; - const correctText = 'No estimate or time spent'; - - expect(this.timeTracker.showNoTimeTrackingState).toBe(true); - expect($noTrackingPane).toBeVisible(); - expect(noTrackingText).toBe(correctText); - done(); - }); - }); - }); - - describe("Help pane", function() { - beforeEach(function() { - initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 }); - }); - - it('should not show the "Help" pane by default', function(done) { - Vue.nextTick(() => { - const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); - - expect(this.timeTracker.showHelpState).toBe(false); - expect($helpPane).toBeNull(); - done(); - }); - }); - - it('should show the "Help" pane when help button is clicked', function(done) { - Vue.nextTick(() => { - $(this.timeTracker.$el).find('.help-button').click(); - - setTimeout(() => { - const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); - expect(this.timeTracker.showHelpState).toBe(true); - expect($helpPane).toBeVisible(); - done(); - }, 10); - }); - }); - - it('should not show the "Help" pane when help button is clicked and then closed', function(done) { - Vue.nextTick(() => { - $(this.timeTracker.$el).find('.help-button').click(); - - setTimeout(() => { - - $(this.timeTracker.$el).find('.close-help-button').click(); - - setTimeout(() => { - const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); - - expect(this.timeTracker.showHelpState).toBe(false); - expect($helpPane).toBeNull(); - - done(); - }, 1000); - }, 1000); - }); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js new file mode 100644 index 00000000000..b58de607ece --- /dev/null +++ b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js @@ -0,0 +1,243 @@ +import $ from 'jquery'; +import Vue from 'vue'; + +import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue'; + +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('Issuable Time Tracker', () => { + let initialData; + let vm; + + const initTimeTrackingComponent = opts => { + setFixtures(` + <div> + <div id="mock-container"></div> + </div> + `); + + initialData = { + time_estimate: opts.timeEstimate, + time_spent: opts.timeSpent, + human_time_estimate: opts.timeEstimateHumanReadable, + human_time_spent: opts.timeSpentHumanReadable, + rootPath: '/', + }; + + const TimeTrackingComponent = Vue.extend({ + ...TimeTracker, + components: { + ...TimeTracker.components, + transition: { + // disable animations + template: '<div><slot></slot></div>', + }, + }, + }); + vm = mountComponent(TimeTrackingComponent, initialData, '#mock-container'); + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('Initialization', () => { + beforeEach(() => { + initTimeTrackingComponent({ + timeEstimate: 100000, + timeSpent: 5000, + timeEstimateHumanReadable: '2h 46m', + timeSpentHumanReadable: '1h 23m', + }); + }); + + it('should return something defined', () => { + expect(vm).toBeDefined(); + }); + + it('should correctly set timeEstimate', done => { + Vue.nextTick(() => { + expect(vm.timeEstimate).toBe(initialData.time_estimate); + done(); + }); + }); + + it('should correctly set time_spent', done => { + Vue.nextTick(() => { + expect(vm.timeSpent).toBe(initialData.time_spent); + done(); + }); + }); + }); + + describe('Content Display', () => { + describe('Panes', () => { + describe('Comparison pane', () => { + beforeEach(() => { + initTimeTrackingComponent({ + timeEstimate: 100000, + timeSpent: 5000, + timeEstimateHumanReadable: '', + timeSpentHumanReadable: '', + }); + }); + + it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', done => { + Vue.nextTick(() => { + expect(vm.showComparisonState).toBe(true); + const $comparisonPane = vm.$el.querySelector('.time-tracking-comparison-pane'); + expect($comparisonPane).toBeVisible(); + done(); + }); + }); + + describe('Remaining meter', () => { + it('should display the remaining meter with the correct width', done => { + Vue.nextTick(() => { + const meterWidth = vm.$el.querySelector('.time-tracking-comparison-pane .meter-fill') + .style.width; + const correctWidth = '5%'; + + expect(meterWidth).toBe(correctWidth); + done(); + }); + }); + + it('should display the remaining meter with the correct background color when within estimate', done => { + Vue.nextTick(() => { + const styledMeter = $(vm.$el).find( + '.time-tracking-comparison-pane .within_estimate .meter-fill', + ); + expect(styledMeter.length).toBe(1); + done(); + }); + }); + + it('should display the remaining meter with the correct background color when over estimate', done => { + vm.time_estimate = 100000; + vm.time_spent = 20000000; + Vue.nextTick(() => { + const styledMeter = $(vm.$el).find( + '.time-tracking-comparison-pane .over_estimate .meter-fill', + ); + expect(styledMeter.length).toBe(1); + done(); + }); + }); + }); + }); + + describe('Estimate only pane', () => { + beforeEach(() => { + initTimeTrackingComponent({ + timeEstimate: 100000, + timeSpent: 0, + timeEstimateHumanReadable: '2h 46m', + timeSpentHumanReadable: '', + }); + }); + + it('should display the human readable version of time estimated', done => { + Vue.nextTick(() => { + const estimateText = vm.$el.querySelector('.time-tracking-estimate-only-pane') + .innerText; + const correctText = 'Estimated: 2h 46m'; + + expect(estimateText).toBe(correctText); + done(); + }); + }); + }); + + describe('Spent only pane', () => { + beforeEach(() => { + initTimeTrackingComponent({ + timeEstimate: 0, + timeSpent: 5000, + timeEstimateHumanReadable: '2h 46m', + timeSpentHumanReadable: '1h 23m', + }); + }); + + it('should display the human readable version of time spent', done => { + Vue.nextTick(() => { + const spentText = vm.$el.querySelector('.time-tracking-spend-only-pane').innerText; + const correctText = 'Spent: 1h 23m'; + + expect(spentText).toBe(correctText); + done(); + }); + }); + }); + + describe('No time tracking pane', () => { + beforeEach(() => { + initTimeTrackingComponent({ + timeEstimate: 0, + timeSpent: 0, + timeEstimateHumanReadable: '', + timeSpentHumanReadable: '', + }); + }); + + it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', done => { + Vue.nextTick(() => { + const $noTrackingPane = vm.$el.querySelector('.time-tracking-no-tracking-pane'); + const noTrackingText = $noTrackingPane.innerText; + const correctText = 'No estimate or time spent'; + + expect(vm.showNoTimeTrackingState).toBe(true); + expect($noTrackingPane).toBeVisible(); + expect(noTrackingText).toBe(correctText); + done(); + }); + }); + }); + + describe('Help pane', () => { + const helpButton = () => vm.$el.querySelector('.help-button'); + const closeHelpButton = () => vm.$el.querySelector('.close-help-button'); + const helpPane = () => vm.$el.querySelector('.time-tracking-help-state'); + + beforeEach(done => { + initTimeTrackingComponent({ timeEstimate: 0, timeSpent: 0 }); + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('should not show the "Help" pane by default', () => { + expect(vm.showHelpState).toBe(false); + expect(helpPane()).toBeNull(); + }); + + it('should show the "Help" pane when help button is clicked', done => { + helpButton().click(); + + Vue.nextTick() + .then(() => { + expect(vm.showHelpState).toBe(true); + expect(helpPane()).toBeVisible(); + }) + .then(done) + .catch(done.fail); + }); + + it('should not show the "Help" pane when help button is clicked and then closed', done => { + helpButton().click(); + + Vue.nextTick() + .then(() => closeHelpButton().click()) + .then(() => Vue.nextTick()) + .then(() => { + expect(vm.showHelpState).toBe(false); + expect(helpPane()).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + }); +}); diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index ec1a684cfbc..a8c5627e678 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -2,6 +2,11 @@ require "spec_helper" describe Gitlab::Git::Branch, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:rugged) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged + end + end subject { repository.branches } @@ -124,6 +129,7 @@ describe Gitlab::Git::Branch, seed_helper: true do it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) } def create_commit - repository.create_commit(params.merge(committer: committer.merge(time: Time.now))) + params[:message].delete!("\r") + Rugged::Commit.create(rugged, params.merge(committer: committer.merge(time: Time.now))) end end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 3bb0b5be15b..7c3d2af819b 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -27,6 +27,7 @@ EOT too_large: false } + # TODO use a Gitaly diff object instead @rugged_diff = Gitlab::GitalyClient::StorageSettings.allow_disk_access do repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths: [".gitmodules"]).patches.first @@ -266,8 +267,12 @@ EOT describe '#submodule?' do before do - commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e') - @diffs = commit.parents[0].diff(commit).patches + # TODO use a Gitaly diff object instead + rugged_commit = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e') + end + + @diffs = rugged_commit.parents[0].diff(rugged_commit).patches end it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 4f8e6f29255..f155ebc9d1a 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -611,21 +611,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#remove_remote" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.remove_remote("expendable") - end - - it "should remove the remote" do - expect(@repo.rugged.remotes).not_to include("expendable") - end - - after(:all) do - ensure_seeds - end - end - describe "#remote_update" do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') @@ -633,7 +618,9 @@ describe Gitlab::Git::Repository, seed_helper: true do end it "should add the remote" do - expect(@repo.rugged.remotes["expendable"].url).to( + rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access { @repo.rugged } + + expect(rugged.remotes["expendable"].url).to( eq(TEST_NORMAL_REPO_PATH) ) end @@ -1157,6 +1144,13 @@ describe Gitlab::Git::Repository, seed_helper: true do @repo.rugged.config['core.autocrlf'] = true end + around do |example| + # OK because autocrlf is only used in gitaly-ruby + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + it 'return the value of the autocrlf option' do expect(@repo.autocrlf).to be(true) end @@ -1172,6 +1166,13 @@ describe Gitlab::Git::Repository, seed_helper: true do @repo.rugged.config['core.autocrlf'] = false end + around do |example| + # OK because autocrlf= is only used in gitaly-ruby + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + it 'should set the autocrlf option to the provided option' do @repo.autocrlf = :input @@ -2042,54 +2043,61 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) do Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end + let(:rugged) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.rugged } + end let(:remote_name) { 'my-remote' } + let(:url) { 'http://my-repo.git' } after do ensure_seeds end describe '#add_remote' do - let(:url) { 'http://my-repo.git' } let(:mirror_refmap) { '+refs/*:refs/*' } - it 'creates a new remote via Gitaly' do - expect_any_instance_of(Gitlab::GitalyClient::RemoteService) - .to receive(:add_remote).with(remote_name, url, mirror_refmap) + shared_examples 'add_remote' do + it 'added the remote' do + begin + rugged.remotes.delete(remote_name) + rescue Rugged::ConfigError + end + + repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) - repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) + expect(rugged.remotes[remote_name]).not_to be_nil + expect(rugged.config["remote.#{remote_name}.mirror"]).to eq('true') + expect(rugged.config["remote.#{remote_name}.prune"]).to eq('true') + expect(rugged.config["remote.#{remote_name}.fetch"]).to eq(mirror_refmap) + end end - context 'with Gitaly disabled', :skip_gitaly_mock do - it 'creates a new remote via Rugged' do - expect_any_instance_of(Rugged::RemoteCollection).to receive(:create) - .with(remote_name, url) - expect_any_instance_of(Rugged::Config).to receive(:[]=) - .with("remote.#{remote_name}.mirror", true) - expect_any_instance_of(Rugged::Config).to receive(:[]=) - .with("remote.#{remote_name}.prune", true) - expect_any_instance_of(Rugged::Config).to receive(:[]=) - .with("remote.#{remote_name}.fetch", mirror_refmap) + context 'using Gitaly' do + it_behaves_like 'add_remote' + end - repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) - end + context 'with Gitaly disabled', :disable_gitaly do + it_behaves_like 'add_remote' end end describe '#remove_remote' do - it 'removes the remote via Gitaly' do - expect_any_instance_of(Gitlab::GitalyClient::RemoteService) - .to receive(:remove_remote).with(remote_name) + shared_examples 'remove_remote' do + it 'removes the remote' do + rugged.remotes.create(remote_name, url) - repository.remove_remote(remote_name) + repository.remove_remote(remote_name) + + expect(rugged.remotes[remote_name]).to be_nil + end end - context 'with Gitaly disabled', :skip_gitaly_mock do - it 'removes the remote via Rugged' do - expect_any_instance_of(Rugged::RemoteCollection).to receive(:delete) - .with(remote_name) + context 'using Gitaly' do + it_behaves_like 'remove_remote' + end - repository.remove_remote(remote_name) - end + context 'with Gitaly disabled', :disable_gitaly do + it_behaves_like 'remove_remote' end end end @@ -2281,20 +2289,25 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') } it 'cleans up the files' do - repository.with_worktree(worktree_path, 'master', env: ENV) do - FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) - # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, - # but the HEAD must be 40 characters long or git will ignore it. - File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) + create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master] + raise 'preparation failed' unless system(*create_worktree, err: '/dev/null') - # git 2.16 fails with "fatal: bad object HEAD" - expect { repository.rev_list(including: :all) }.to raise_error(Gitlab::Git::Repository::GitError) + FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) + # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, + # but the HEAD must be 40 characters long or git will ignore it. + File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) - repository.clean_stale_repository_files + # git 2.16 fails with "fatal: bad object HEAD" + expect(rev_list_all).to be false - expect { repository.rev_list(including: :all) }.not_to raise_error - expect(File.exist?(worktree_path)).to be_falsey - end + repository.clean_stale_repository_files + + expect(rev_list_all).to be true + expect(File.exist?(worktree_path)).to be_falsey + end + + def rev_list_all + system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null') end it 'increments a counter upon an error' do diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index b752c3e8341..d9d405e1ccc 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -32,65 +32,4 @@ describe Gitlab::Git::RevList do expect(rev_list.new_refs).to eq(%w[sha1 sha2]) end end - - context '#new_objects' do - it 'fetches list of newly pushed objects using rev-list' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.new_objects(&b) }.to yield_with_args(%w[sha1 sha2]) - end - - it 'can skip pathless objects' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file") - - expect { |b| rev_list.new_objects(require_path: true, &b) }.to yield_with_args(%w[sha2]) - end - - it 'can handle non utf-8 paths' do - non_utf_char = [0x89].pack("c*").force_encoding("UTF-8") - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1") - - rev_list.new_objects(require_path: true) do |object_ids| - expect(object_ids.force).to eq(%w[sha2]) - end - end - - it 'can yield a lazy enumerator' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") - - rev_list.new_objects do |object_ids| - expect(object_ids).to be_a Enumerator::Lazy - end - end - - it 'returns the result of the block when given' do - stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") - - objects = rev_list.new_objects do |object_ids| - object_ids.first - end - - expect(objects).to eq 'sha1' - end - - it 'can accept list of references to exclude' do - stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.new_objects(not_in: ['master'], &b) }.to yield_with_args(%w[sha1 sha2]) - end - - it 'handles empty list of references to exclude as listing all known objects' do - stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.new_objects(not_in: [], &b) }.to yield_with_args(%w[sha1 sha2]) - end - end - - context '#all_objects' do - it 'fetches list of all pushed objects using rev-list' do - stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2") - - expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2]) - end - end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 832db2bd906..dbd64c4bec0 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -734,26 +734,18 @@ describe Gitlab::GitAccess do merge_into_protected_branch: "0b4bc9a #{merge_into_protected_branch} refs/heads/feature" } end - def stub_git_hooks - # Running the `pre-receive` hook is expensive, and not necessary for this test. - allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) do |service, &block| - block.call(service) - end - end - def merge_into_protected_branch @protected_branch_merge_commit ||= begin Gitlab::GitalyClient::StorageSettings.allow_disk_access do - stub_git_hooks project.repository.add_branch(user, unprotected_branch, 'feature') - target_branch = project.repository.lookup('feature') + rugged = project.repository.rugged + target_branch = rugged.rev_parse('feature') source_branch = project.repository.create_file( user, 'filename', 'This is the file content', message: 'This is a good commit message', branch_name: unprotected_branch) - rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } merge_index = rugged.merge_commits(target_branch, source_branch) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index caf5d829d21..04d00023b8a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -151,7 +151,9 @@ describe Repository do it { is_expected.to eq(['v1.1.0', 'v1.0.0', annotated_tag_name]) } after do - repository.rugged.tags.delete(annotated_tag_name) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.tags.delete(annotated_tag_name) + end end end end @@ -2231,8 +2233,11 @@ describe Repository do create_remote_branch('joe', 'remote_branch', masterrev) repository.add_branch(user, 'local_branch', masterrev.id) - expect(repository.remote_branches('joe').any? { |branch| branch.name == 'local_branch' }).to eq(false) - expect(repository.remote_branches('joe').any? { |branch| branch.name == 'remote_branch' }).to eq(true) + # TODO: move this test to gitaly https://gitlab.com/gitlab-org/gitaly/issues/1243 + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + expect(repository.remote_branches('joe').any? { |branch| branch.name == 'local_branch' }).to eq(false) + expect(repository.remote_branches('joe').any? { |branch| branch.name == 'remote_branch' }).to eq(true) + end end end diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes.rb index 79b2196660c..1b563021244 100644 --- a/spec/support/shared_examples/requests/api/notes.rb +++ b/spec/support/shared_examples/requests/api/notes.rb @@ -121,6 +121,7 @@ shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + expect(Time.parse(json_response['updated_at'])).to be_like_time(creation_time) end end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index d5808e21271..30e67e67e0e 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -209,12 +209,7 @@ describe GitGarbageCollectWorker do tree: old_commit.tree, parents: [old_commit] ) - Gitlab::Git::OperationService.new(nil, project.repository.raw_repository).send( - :update_ref, - "refs/heads/#{SecureRandom.hex(6)}", - new_commit_sha, - Gitlab::Git::BLANK_SHA - ) + rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha) end def packs(project) |