diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index caeecb25227..84c9191975e 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,21 +1,19 @@ diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue index 1218941065f..a25b436b8de 100644 --- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue +++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue @@ -11,7 +11,7 @@ import ProjectSelect from './project_select_deprecated.vue'; // This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards export default { - name: 'BoardNewIssue', + name: 'BoardNewIssueDeprecated', components: { ProjectSelect, GlButton, diff --git a/app/assets/javascripts/boards/components/board_new_item.vue b/app/assets/javascripts/boards/components/board_new_item.vue new file mode 100644 index 00000000000..44574de17d7 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_new_item.vue @@ -0,0 +1,95 @@ + + + diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb index c15b0ed6a1b..55fd68fd6e8 100644 --- a/lib/backup/gitaly_backup.rb +++ b/lib/backup/gitaly_backup.rb @@ -25,18 +25,21 @@ module Backup args += ['-parallel', @parallel.to_s] if type == :create && @parallel args += ['-parallel-storage', @parallel_storage.to_s] if type == :create && @parallel_storage - @read_io, @write_io = IO.pipe - @pid = Process.spawn(bin_path, command, '-path', backup_repos_path, *args, in: @read_io, out: @progress) + @stdin, stdout, @thread = Open3.popen2(ENV, bin_path, command, '-path', backup_repos_path, *args) + + @out_reader = Thread.new do + IO.copy_stream(stdout, @progress) + end end def wait return unless started? - @write_io.close - Process.wait(@pid) - status = $? + @stdin.close + [@thread, @out_reader].each(&:join) + status = @thread.value - @pid = nil + @thread = nil raise Error, "gitaly-backup exit status #{status.exitstatus}" if status.exitstatus != 0 end @@ -46,7 +49,7 @@ module Backup repository = repo_type.repository_for(container) - @write_io.puts({ + @stdin.puts({ storage_name: repository.storage, relative_path: repository.relative_path, gl_project_path: repository.gl_project_path, @@ -61,7 +64,7 @@ module Backup private def started? - @pid.present? + @thread.present? end def backup_repos_path diff --git a/lib/gitlab/background_migration/backfill_snippet_repositories.rb b/lib/gitlab/background_migration/backfill_snippet_repositories.rb index 6f37f1846d2..b58f0a3a3e0 100644 --- a/lib/gitlab/background_migration/backfill_snippet_repositories.rb +++ b/lib/gitlab/background_migration/backfill_snippet_repositories.rb @@ -105,7 +105,7 @@ module Gitlab end def commit_attrs - @commit_attrs ||= { branch_name: 'master', message: 'Initial commit' } + @commit_attrs ||= { branch_name: 'main', message: 'Initial commit' } end def create_commit(snippet) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7f2d1510841..71e5515ad96 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5384,9 +5384,6 @@ msgstr "" msgid "Board|Enter board name" msgstr "" -msgid "Board|Failed to create epic. Please try again." -msgstr "" - msgid "Board|Failed to delete board. Please try again." msgstr "" diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb index 4b7669810ec..375a371c2b1 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :registry, :orchestrated do + RSpec.describe 'Package', :registry, :orchestrated, only: { pipeline: :main } do describe 'Self-managed Container Registry' do let(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index c440c110094..811f0043a01 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -4,8 +4,9 @@ import Vuex from 'vuex'; import BoardCard from '~/boards/components/board_card.vue'; import BoardList from '~/boards/components/board_list.vue'; import BoardNewIssue from '~/boards/components/board_new_issue.vue'; +import BoardNewItem from '~/boards/components/board_new_item.vue'; import defaultState from '~/boards/stores/state'; -import { mockList, mockIssuesByListId, issues } from './mock_data'; +import { mockList, mockIssuesByListId, issues, mockGroupProjects } from './mock_data'; export default function createComponent({ listIssueProps = {}, @@ -17,6 +18,7 @@ export default function createComponent({ state = defaultState, stubs = { BoardNewIssue, + BoardNewItem, BoardCard, }, } = {}) { @@ -25,6 +27,7 @@ export default function createComponent({ const store = new Vuex.Store({ state: { + selectedProject: mockGroupProjects[0], boardItemsByListId: mockIssuesByListId, boardItems: issues, pageInfoByListId: { @@ -77,6 +80,7 @@ export default function createComponent({ provide: { groupId: null, rootPath: '/', + boardId: '1', weightFeatureAvailable: false, boardWeight: null, canAdminList: true, diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index e6405bbcff3..57ccebf3676 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -1,6 +1,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; import BoardNewIssue from '~/boards/components/board_new_issue.vue'; +import BoardNewItem from '~/boards/components/board_new_item.vue'; +import ProjectSelect from '~/boards/components/project_select.vue'; +import eventHub from '~/boards/eventhub'; import { mockList, mockGroupProjects } from '../mock_data'; @@ -8,107 +11,104 @@ const localVue = createLocalVue(); localVue.use(Vuex); +const addListNewIssuesSpy = jest.fn().mockResolvedValue(); +const mockActions = { addListNewIssue: addListNewIssuesSpy }; + +const createComponent = ({ + state = { selectedProject: mockGroupProjects[0], fullPath: mockGroupProjects[0].fullPath }, + actions = mockActions, + getters = { isGroupBoard: () => true, isProjectBoard: () => false }, +} = {}) => + shallowMount(BoardNewIssue, { + localVue, + store: new Vuex.Store({ + state, + actions, + getters, + }), + propsData: { + list: mockList, + }, + provide: { + groupId: 1, + weightFeatureAvailable: false, + boardWeight: null, + }, + stubs: { + BoardNewItem, + }, + }); + describe('Issue boards new issue form', () => { let wrapper; - let vm; - const addListNewIssuesSpy = jest.fn(); + const findBoardNewItem = () => wrapper.findComponent(BoardNewItem); - const findSubmitButton = () => wrapper.find({ ref: 'submitButton' }); - const findCancelButton = () => wrapper.find({ ref: 'cancelButton' }); - const findSubmitForm = () => wrapper.find({ ref: 'submitForm' }); + beforeEach(async () => { + wrapper = createComponent(); - const submitIssue = () => { - const dummySubmitEvent = { - preventDefault() {}, - }; - - return findSubmitForm().trigger('submit', dummySubmitEvent); - }; - - beforeEach(() => { - const store = new Vuex.Store({ - state: { selectedProject: mockGroupProjects[0] }, - actions: { addListNewIssue: addListNewIssuesSpy }, - getters: { isGroupBoard: () => false, isProjectBoard: () => true }, - }); - - wrapper = shallowMount(BoardNewIssue, { - propsData: { - disabled: false, - list: mockList, - }, - store, - localVue, - provide: { - groupId: null, - weightFeatureAvailable: false, - boardWeight: null, - }, - }); - - vm = wrapper.vm; - - return vm.$nextTick(); + await wrapper.vm.$nextTick(); }); afterEach(() => { wrapper.destroy(); }); - it('calls submit if submit button is clicked', async () => { - jest.spyOn(wrapper.vm, 'submit').mockImplementation(); - wrapper.setData({ title: 'Testing Title' }); - - await vm.$nextTick(); - await submitIssue(); - expect(wrapper.vm.submit).toHaveBeenCalled(); + it('renders board-new-item component', () => { + const boardNewItem = findBoardNewItem(); + expect(boardNewItem.exists()).toBe(true); + expect(boardNewItem.props()).toEqual({ + list: mockList, + formEventPrefix: 'toggle-issue-form-', + submitButtonTitle: 'Create issue', + disableSubmit: false, + }); }); - it('disables submit button if title is empty', () => { - expect(findSubmitButton().props().disabled).toBe(true); + it('calls addListNewIssue action when `board-new-item` emits form-submit event', async () => { + findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' }); + + await wrapper.vm.$nextTick(); + expect(addListNewIssuesSpy).toHaveBeenCalledWith(expect.any(Object), { + list: mockList, + issueInput: { + title: 'Foo', + labelIds: [], + assigneeIds: [], + milestoneId: undefined, + projectPath: mockGroupProjects[0].fullPath, + }, + }); }); - it('enables submit button if title is not empty', async () => { - wrapper.setData({ title: 'Testing Title' }); + it('emits event `toggle-issue-form` with current list Id suffix on eventHub when `board-new-item` emits form-cancel event', async () => { + jest.spyOn(eventHub, '$emit').mockImplementation(); + findBoardNewItem().vm.$emit('form-cancel'); - await vm.$nextTick(); - expect(wrapper.find({ ref: 'input' }).element.value).toBe('Testing Title'); - expect(findSubmitButton().props().disabled).toBe(false); + await wrapper.vm.$nextTick(); + expect(eventHub.$emit).toHaveBeenCalledWith(`toggle-issue-form-${mockList.id}`); }); - it('clears title after clicking cancel', async () => { - findCancelButton().trigger('click'); + describe('when in group issue board', () => { + it('renders project-select component within board-new-item component', () => { + const projectSelect = findBoardNewItem().findComponent(ProjectSelect); - await vm.$nextTick(); - expect(vm.title).toBe(''); + expect(projectSelect.exists()).toBe(true); + expect(projectSelect.props('list')).toEqual(mockList); + }); }); - describe('submit success', () => { - it('creates new issue', async () => { - wrapper.setData({ title: 'create issue' }); - - await vm.$nextTick(); - await submitIssue(); - expect(addListNewIssuesSpy).toHaveBeenCalled(); + describe('when in project issue board', () => { + beforeEach(() => { + wrapper = createComponent({ + getters: { isGroupBoard: () => false, isProjectBoard: () => true }, + }); }); - it('enables button after submit', async () => { - jest.spyOn(wrapper.vm, 'submit').mockImplementation(); - wrapper.setData({ title: 'create issue' }); + it('does not render project-select component within board-new-item component', () => { + const projectSelect = findBoardNewItem().findComponent(ProjectSelect); - await vm.$nextTick(); - await submitIssue(); - expect(findSubmitButton().props().disabled).toBe(false); - }); - - it('clears title after submit', async () => { - wrapper.setData({ title: 'create issue' }); - - await vm.$nextTick(); - await submitIssue(); - await vm.$nextTick(); - expect(vm.title).toBe(''); + expect(projectSelect.exists()).toBe(false); }); }); }); diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js new file mode 100644 index 00000000000..0151d9c1c14 --- /dev/null +++ b/spec/frontend/boards/components/board_new_item_spec.js @@ -0,0 +1,103 @@ +import { GlForm, GlFormInput, GlButton } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +import BoardNewItem from '~/boards/components/board_new_item.vue'; +import eventHub from '~/boards/eventhub'; + +import { mockList } from '../mock_data'; + +const createComponent = ({ + list = mockList, + formEventPrefix = 'toggle-issue-form-', + disabledSubmit = false, + submitButtonTitle = 'Create item', +} = {}) => + mountExtended(BoardNewItem, { + propsData: { + list, + formEventPrefix, + disabledSubmit, + submitButtonTitle, + }, + slots: { + default: '
', + }, + stubs: { + GlForm, + }, + }); + +describe('BoardNewItem', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('template', () => { + it('renders gl-form component', () => { + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + }); + + it('renders field label', () => { + expect(wrapper.find('label').exists()).toBe(true); + expect(wrapper.find('label').text()).toBe('Title'); + }); + + it('renders gl-form-input field', () => { + expect(wrapper.findComponent(GlFormInput).exists()).toBe(true); + }); + + it('renders default slot contents', () => { + expect(wrapper.find('#default-slot').exists()).toBe(true); + }); + + it('renders submit and cancel buttons', () => { + const buttons = wrapper.findAllComponents(GlButton); + expect(buttons).toHaveLength(2); + expect(buttons.at(0).text()).toBe('Create item'); + expect(buttons.at(1).text()).toBe('Cancel'); + }); + + describe('events', () => { + const glForm = () => wrapper.findComponent(GlForm); + const titleInput = () => wrapper.find('input[name="issue_title"]'); + + it('emits `form-submit` event with title value when `submit` is triggered on gl-form', async () => { + titleInput().setValue('Foo'); + await glForm().trigger('submit'); + + expect(wrapper.emitted('form-submit')).toBeTruthy(); + expect(wrapper.emitted('form-submit')[0]).toEqual([ + { + title: 'Foo', + list: mockList, + }, + ]); + }); + + it('emits `scroll-board-list-` event with list.id on eventHub when `submit` is triggered on gl-form', async () => { + jest.spyOn(eventHub, '$emit').mockImplementation(); + await glForm().trigger('submit'); + + expect(eventHub.$emit).toHaveBeenCalledWith(`scroll-board-list-${mockList.id}`); + }); + + it('emits `form-cancel` event and clears title value when `reset` is triggered on gl-form', async () => { + titleInput().setValue('Foo'); + + await wrapper.vm.$nextTick(); + expect(titleInput().element.value).toBe('Foo'); + + await glForm().trigger('reset'); + + expect(titleInput().element.value).toBe(''); + expect(wrapper.emitted('form-cancel')).toBeTruthy(); + }); + }); + }); +}); diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb index cdb35c0ce01..a48a1752eff 100644 --- a/spec/lib/backup/gitaly_backup_spec.rb +++ b/spec/lib/backup/gitaly_backup_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Backup::GitalyBackup do project_snippet = create(:project_snippet, :repository, project: project) personal_snippet = create(:personal_snippet, :repository, author: project.owner) - expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, { in: anything, out: progress }).and_call_original + expect(Open3).to receive(:popen2).with(ENV, anything, 'create', '-path', anything).and_call_original subject.start(:create) subject.enqueue(project, Gitlab::GlRepository::PROJECT) @@ -53,7 +53,7 @@ RSpec.describe Backup::GitalyBackup do let(:parallel) { 3 } it 'passes parallel option through' do - expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, '-parallel', '3', { in: anything, out: progress }).and_call_original + expect(Open3).to receive(:popen2).with(ENV, anything, 'create', '-path', anything, '-parallel', '3').and_call_original subject.start(:create) subject.wait @@ -64,7 +64,7 @@ RSpec.describe Backup::GitalyBackup do let(:parallel_storage) { 3 } it 'passes parallel option through' do - expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, '-parallel-storage', '3', { in: anything, out: progress }).and_call_original + expect(Open3).to receive(:popen2).with(ENV, anything, 'create', '-path', anything, '-parallel-storage', '3').and_call_original subject.start(:create) subject.wait @@ -109,7 +109,7 @@ RSpec.describe Backup::GitalyBackup do copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle') copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle') - expect(Process).to receive(:spawn).with(anything, 'restore', '-path', anything, { in: anything, out: progress }).and_call_original + expect(Open3).to receive(:popen2).with(ENV, anything, 'restore', '-path', anything).and_call_original subject.start(:restore) subject.enqueue(project, Gitlab::GlRepository::PROJECT) @@ -132,7 +132,7 @@ RSpec.describe Backup::GitalyBackup do let(:parallel) { 3 } it 'does not pass parallel option through' do - expect(Process).to receive(:spawn).with(anything, 'restore', '-path', anything, { in: anything, out: progress }).and_call_original + expect(Open3).to receive(:popen2).with(ENV, anything, 'restore', '-path', anything).and_call_original subject.start(:restore) subject.wait diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index dbf74bd9333..d22aa86dbe0 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -304,7 +304,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat end def blob_at(snippet, path) - raw_repository(snippet).blob_at('master', path) + raw_repository(snippet).blob_at('main', path) end def repository_exists?(snippet) diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index ebaaf179546..aecdc73ee3d 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -412,6 +412,16 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process end end + + context 'CRON env is set' do + before do + stub_env('CRON', '1') + end + + it 'does not output to stdout' do + expect { run_rake_task('gitlab:backup:create') }.not_to output.to_stdout_from_any_process + end + end end # backup_create task