diff options
author | Felipe Artur <fcardozo@gitlab.com> | 2018-03-06 16:28:54 +0000 |
---|---|---|
committer | Felipe Artur <fcardozo@gitlab.com> | 2018-03-06 16:28:54 +0000 |
commit | e77c4e9efe0e19187929e5836cda5a3a59d0f89f (patch) | |
tree | 91daaa89bb48457456f931c6b818f5e200390b56 /spec | |
parent | 1e137c273ca6314d0ed6744910b95f179b1d538c (diff) | |
parent | 9a8f5a2b605f85ace3c81a32cf1855f79cabde43 (diff) | |
download | gitlab-ce-e77c4e9efe0e19187929e5836cda5a3a59d0f89f.tar.gz |
Merge branch 'master' into 'issue_38337'
# Conflicts:
# app/models/group.rb
# db/schema.rb
Diffstat (limited to 'spec')
195 files changed, 4531 insertions, 5775 deletions
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index b7257fac608..fb6d82d7de3 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -246,7 +246,7 @@ describe AutocompleteController do expect(json_response.size).to eq(1) expect(json_response.first['id']).to eq authorized_project.id - expect(json_response.first['name_with_namespace']).to eq authorized_project.name_with_namespace + expect(json_response.first['name_with_namespace']).to eq authorized_project.full_name end end end @@ -267,7 +267,7 @@ describe AutocompleteController do expect(json_response.size).to eq(1) expect(json_response.first['id']).to eq authorized_search_project.id - expect(json_response.first['name_with_namespace']).to eq authorized_search_project.name_with_namespace + expect(json_response.first['name_with_namespace']).to eq authorized_search_project.full_name end end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 734396ddf7b..3b9e06cb5ad 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -407,10 +407,43 @@ describe Projects::BranchesController do get :index, namespace_id: project.namespace, project_id: project, + state: 'all', format: :html expect(response).to have_gitlab_http_status(200) end end + + context 'when depreated sort/search/page parameters are specified' do + it 'returns with a status 301 when sort specified' do + get :index, + namespace_id: project.namespace, + project_id: project, + sort: 'updated_asc', + format: :html + + expect(response).to redirect_to project_branches_filtered_path(project, state: 'all') + end + + it 'returns with a status 301 when search specified' do + get :index, + namespace_id: project.namespace, + project_id: project, + search: 'feature', + format: :html + + expect(response).to redirect_to project_branches_filtered_path(project, state: 'all') + end + + it 'returns with a status 301 when page specified' do + get :index, + namespace_id: project.namespace, + project_id: project, + page: 2, + format: :html + + expect(response).to redirect_to project_branches_filtered_path(project, state: 'all') + end + end end end diff --git a/spec/factories/badge.rb b/spec/factories/badge.rb new file mode 100644 index 00000000000..b87ece946cb --- /dev/null +++ b/spec/factories/badge.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + trait :base_badge do + link_url { generate(:url) } + image_url { generate(:url) } + end + + factory :project_badge, traits: [:base_badge], class: ProjectBadge do + project + end + + factory :group_badge, aliases: [:badge], traits: [:base_badge], class: GroupBadge do + group + end +end diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index 8eb709022ce..caaed4d5246 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -9,4 +9,10 @@ FactoryBot.define do trait :with_file do file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end + + # The uniqueness constraint means we can't use the correct OID for all LFS + # objects, so the test needs to decide which (if any) object gets it + trait :correct_oid do + oid 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75' + end end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index a5f22848031..d5e603baeae 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -173,7 +173,7 @@ feature 'Admin Groups' do visit admin_group_path(group) - expect(page).to have_content(empty_project.name_with_namespace) + expect(page).to have_content(empty_project.full_name) expect(page).to have_content('Projects shared with') end end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index d02ac6c2e2a..6d8350e99f1 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -58,7 +58,7 @@ describe "Admin::Projects" do expect(current_path).to eq admin_project_path(project) expect(page).to have_content(project.path) expect(page).to have_content(project.name) - expect(page).to have_content(project.name_with_namespace) + expect(page).to have_content(project.full_name) expect(page).to have_content(project.creator.name) end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 7eeed7da998..8de2e3d199b 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -76,8 +76,8 @@ describe "Admin Runners" do describe 'projects' do it 'contains project names' do - expect(page).to have_content(@project1.name_with_namespace) - expect(page).to have_content(@project2.name_with_namespace) + expect(page).to have_content(@project1.full_name) + expect(page).to have_content(@project2.full_name) end end @@ -89,8 +89,8 @@ describe "Admin Runners" do end it 'contains name of correct project' do - expect(page).to have_content(@project1.name_with_namespace) - expect(page).not_to have_content(@project2.name_with_namespace) + expect(page).to have_content(@project1.full_name) + expect(page).not_to have_content(@project2.full_name) end end diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 54652e2d849..8d1d5a51750 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -74,8 +74,8 @@ RSpec.describe 'Dashboard Issues' do find('.new-project-item-select-button').click page.within('.select2-results') do - expect(page).to have_content(project.name_with_namespace) - expect(page).not_to have_content(project_with_issues_disabled.name_with_namespace) + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_issues_disabled.full_name) end end @@ -84,8 +84,8 @@ RSpec.describe 'Dashboard Issues' do wait_for_requests - project_path = "/#{project.path_with_namespace}" - project_json = { name: project.name_with_namespace, url: project_path }.to_json + project_path = "/#{project.full_path}" + project_json = { name: project.full_name, url: project_path }.to_json # simulate selection, and prevent overlap by dropdown menu first('.project-item-select', visible: false) diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 744041ac425..c8f3a8449f5 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -28,8 +28,8 @@ feature 'Dashboard Merge Requests' do find('.new-project-item-select-button').click page.within('.select2-results') do - expect(page).to have_content(project.name_with_namespace) - expect(page).not_to have_content(project_with_disabled_merge_requests.name_with_namespace) + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_disabled_merge_requests.full_name) end end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 586c7b48d0b..986f864f0b5 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -37,6 +37,14 @@ feature 'Dashboard Projects' do expect(page).to have_xpath("//time[@datetime='#{project.last_repository_updated_at.getutc.iso8601}']") end + + it 'shows the last_activity_at attribute as the update date' do + project.update_attributes!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.now) + + visit dashboard_projects_path + + expect(page).to have_xpath("//time[@datetime='#{project.last_activity_at.getutc.iso8601}']") + end end context 'when last_repository_updated_at and last_activity_at are missing' do diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb index 2fc34301d51..7b359b0c651 100644 --- a/spec/features/dashboard/todos/todos_filtering_spec.rb +++ b/spec/features/dashboard/todos/todos_filtering_spec.rb @@ -24,14 +24,14 @@ feature 'Dashboard > User filters todos', :js do it 'filters by project' do click_button 'Project' within '.dropdown-menu-project' do - fill_in 'Search projects', with: project_1.name_with_namespace - click_link project_1.name_with_namespace + fill_in 'Search projects', with: project_1.full_name + click_link project_1.full_name end wait_for_requests - expect(page).to have_content project_1.name_with_namespace - expect(page).not_to have_content project_2.name_with_namespace + expect(page).to have_content project_1.full_name + expect(page).not_to have_content project_2.full_name end context 'Author filter' do diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 076a02150a4..3c01ff345fc 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -73,7 +73,7 @@ feature 'issue move to another project' do wait_for_requests page.within '.js-sidebar-move-issue-block' do - expect(page).to have_content new_project.name_with_namespace + expect(page).to have_content new_project.full_name end end end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index e711a191db2..ea7a97d02a0 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -59,7 +59,6 @@ feature 'Issues > User uses quick actions', :js do it 'does not create a note, and sets the due date accordingly' do write_note("/due 2016-08-28") - expect(page).to have_content '/due 2016-08-28' expect(page).not_to have_content 'Commands applied' issue.reload @@ -99,7 +98,6 @@ feature 'Issues > User uses quick actions', :js do it 'does not create a note, and sets the due date accordingly' do write_note("/remove_due_date") - expect(page).to have_content '/remove_due_date' expect(page).not_to have_content 'Commands applied' issue.reload @@ -147,7 +145,6 @@ feature 'Issues > User uses quick actions', :js do it 'does not create a note, and does not mark the issue as a duplicate' do write_note("/duplicate ##{original_issue.to_reference}") - expect(page).to have_content "/duplicate ##{original_issue.to_reference}" expect(page).not_to have_content 'Commands applied' expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index 39bcea013e7..605298ba8ab 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do describe 'when checking branches' do context 'with artifacts' do before do - visit project_branches_path(project, search: 'binary-encoding') + visit project_branches_filtered_path(project, state: 'all', search: 'binary-encoding') end scenario 'shows download artifacts button' do diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 2fddd274078..2a9d9e6416c 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -11,15 +11,109 @@ describe 'Branches' do project.add_developer(user) end - describe 'Initial branches page' do - it 'shows all the branches sorted by last updated by default' do + context 'on the projects with 6 active branches and 4 stale branches' do + let(:project) { create(:project, :public, :empty_repo) } + let(:repository) { project.repository } + let(:threshold) { Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD } + + before do + # Add 4 stale branches + (1..4).reverse_each do |i| + Timecop.freeze((threshold + i).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") } + end + # Add 6 active branches + (1..6).each do |i| + Timecop.freeze((threshold - i).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") } + end + end + + describe 'Overview page of the branches' do + it 'shows the first 5 active branches and the first 4 stale branches sorted by last updated' do + visit project_branches_path(project) + + expect(page).to have_content(sorted_branches(repository, count: 5, sort_by: :updated_desc, state: 'active')) + expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_desc, state: 'stale')) + + expect(page).to have_link('Show more active branches', href: project_branches_filtered_path(project, state: 'active')) + expect(page).not_to have_content('Show more stale branches') + end + end + + describe 'Active branches page' do + it 'shows 6 active branches sorted by last updated' do + visit project_branches_filtered_path(project, state: 'active') + + expect(page).to have_content(sorted_branches(repository, count: 6, sort_by: :updated_desc, state: 'active')) + end + end + + describe 'Stale branches page' do + it 'shows 4 active branches sorted by last updated' do + visit project_branches_filtered_path(project, state: 'stale') + + expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_desc, state: 'stale')) + end + end + + describe 'All branches page' do + it 'shows 10 branches sorted by last updated' do + visit project_branches_filtered_path(project, state: 'all') + + expect(page).to have_content(sorted_branches(repository, count: 10, sort_by: :updated_desc)) + end + end + + context 'with branches over more than one page' do + before do + allow(Kaminari.config).to receive(:default_per_page).and_return(5) + end + + it 'shows only default_per_page active branches sorted by last updated' do + visit project_branches_filtered_path(project, state: 'active') + + expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page, sort_by: :updated_desc, state: 'active')) + end + + it 'shows only default_per_page branches sorted by last updated on All branches' do + visit project_branches_filtered_path(project, state: 'all') + + expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page, sort_by: :updated_desc)) + end + end + end + + describe 'Find branches' do + it 'shows filtered branches', :js do visit project_branches_path(project) + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('fix') + expect(find('.all-branches')).to have_selector('li', count: 1) + end + end + + describe 'Delete unprotected branch on Overview' do + it 'removes branch after confirmation', :js do + visit project_branches_filtered_path(project, state: 'all') + + expect(all('.all-branches').last).to have_selector('li', count: 20) + accept_confirm { find('.js-branch-add-pdf-text-binary .btn-remove').click } + + expect(all('.all-branches').last).to have_selector('li', count: 19) + end + end + + describe 'All branches page' do + it 'shows all the branches sorted by last updated by default' do + visit project_branches_filtered_path(project, state: 'all') + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc)) end it 'sorts the branches by name' do - visit project_branches_path(project) + visit project_branches_filtered_path(project, state: 'all') click_button "Last updated" # Open sorting dropdown click_link "Name" @@ -28,7 +122,7 @@ describe 'Branches' do end it 'sorts the branches by oldest updated' do - visit project_branches_path(project) + visit project_branches_filtered_path(project, state: 'all') click_button "Last updated" # Open sorting dropdown click_link "Oldest updated" @@ -41,13 +135,13 @@ describe 'Branches' do %w(one two three four five).each { |ref| repository.add_branch(user, ref, 'master') } - expect { visit project_branches_path(project) }.not_to exceed_query_limit(control_count) + expect { visit project_branches_filtered_path(project, state: 'all') }.not_to exceed_query_limit(control_count) end end - describe 'Find branches' do + describe 'Find branches on All branches' do it 'shows filtered branches', :js do - visit project_branches_path(project) + visit project_branches_filtered_path(project, state: 'all') fill_in 'branch-search', with: 'fix' find('#branch-search').native.send_keys(:enter) @@ -57,9 +151,9 @@ describe 'Branches' do end end - describe 'Delete unprotected branch' do + describe 'Delete unprotected branch on All branches' do it 'removes branch after confirmation', :js do - visit project_branches_path(project) + visit project_branches_filtered_path(project, state: 'all') fill_in 'branch-search', with: 'fix' @@ -73,6 +167,19 @@ describe 'Branches' do expect(find('.all-branches')).to have_selector('li', count: 0) end end + + context 'on project with 0 branch' do + let(:project) { create(:project, :public, :empty_repo) } + let(:repository) { project.repository } + + describe '0 branches on Overview' do + it 'shows warning' do + visit project_branches_path(project) + + expect(page).not_to have_selector('.all-branches') + end + end + end end context 'logged in as master' do @@ -83,7 +190,7 @@ describe 'Branches' do describe 'Initial branches page' do it 'shows description for admin' do - visit project_branches_path(project) + visit project_branches_filtered_path(project, state: 'all') expect(page).to have_content("Protected branches can be managed in project settings") end @@ -102,12 +209,18 @@ describe 'Branches' do end end - def sorted_branches(repository, count:, sort_by:) + def sorted_branches(repository, count:, sort_by:, state: nil) + branches = repository.branches_sorted_by(sort_by) + branches = branches.select { |b| state == 'active' ? b.active? : b.stale? } if state sorted_branches = - repository.branches_sorted_by(sort_by).first(count).map do |branch| + branches.first(count).map do |branch| Regexp.escape(branch.name) end Regexp.new(sorted_branches.join('.*')) end + + def create_file(message: 'message', branch_name:) + repository.create_file(user, generate(:branch), 'content', message: message, branch_name: branch_name) + end end diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 64e600144e0..b233af83eec 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -234,7 +234,7 @@ feature 'Environment' do end scenario 'user deletes the branch with running environment' do - visit project_branches_path(project, search: 'feature') + visit project_branches_filtered_path(project, state: 'all', search: 'feature') remove_branch_with_hooks(project, user, 'feature') do page.within('.js-branch-feature') { find('a.btn-remove').click } diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 0cc68aff494..12bfcc177c7 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb index d575596937d..1f4eec0a317 100644 --- a/spec/features/projects/members/master_manages_access_requests_spec.rb +++ b/spec/features/projects/members/master_manages_access_requests_spec.rb @@ -25,7 +25,7 @@ feature 'Projects > Members > Master manages access requests' do perform_enqueued_jobs { click_on 'Grant access' } expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] - expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted" + expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was granted" end scenario 'master can deny access' do @@ -36,7 +36,7 @@ feature 'Projects > Members > Master manages access requests' do perform_enqueued_jobs { click_on 'Deny access' } expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] - expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied" + expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was denied" end def expect_visible_access_request(project, user) diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 4eb36156812..672d5daa3d8 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -21,7 +21,7 @@ feature 'Projects > Members > User requests access', :js do perform_enqueued_jobs { click_link 'Request Access' } expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email] - expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project" + expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.full_name} project" expect(project.requesters.exists?(user_id: user)).to be_truthy expect(page).to have_content 'Your request for access has been queued for review.' diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 85d518c0cc3..40689964b91 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -81,8 +81,8 @@ feature 'Merge Request button' do context 'on branches page' do it_behaves_like 'Merge request button only shown when allowed' do let(:label) { 'Merge request' } - let(:url) { project_branches_path(project, search: 'feature') } - let(:fork_url) { project_branches_path(forked_project, search: 'feature') } + let(:url) { project_branches_filtered_path(project, state: 'all', search: 'feature') } + let(:fork_url) { project_branches_filtered_path(forked_project, state: 'all', search: 'feature') } end end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index b5104747d00..fd561288091 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -142,7 +142,7 @@ feature 'New project' do context 'from git repository url, "Repo by URL"' do before do - first('.import_git').click + first('.js-import-git-toggle-button').click end it 'does not autocomplete sensitive git repo URL' do diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 2a0d235ef04..233d2e67b9d 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -258,7 +258,7 @@ feature 'Pages' do end let(:ci_build) do - build( + create( :ci_build, project: project, pipeline: pipeline, diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 3a8e7c05cc4..849d85061df 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -86,7 +86,22 @@ describe 'Pipelines', :js do it 'updates content when tab is clicked' do page.find('.js-pipelines-tab-pending').click wait_for_requests - expect(page).to have_content('No pipelines to show.') + expect(page).to have_content('There are currently no pending pipelines.') + end + end + + context 'navigation links' do + before do + visit project_pipelines_path(project) + wait_for_requests + end + + it 'renders run pipeline link' do + expect(page).to have_link('Run Pipeline') + end + + it 'renders ci lint link' do + expect(page).to have_link('CI Lint') end end @@ -542,7 +557,7 @@ describe 'Pipelines', :js do end it 'has a clear caches button' do - expect(page).to have_link 'Clear runner caches' + expect(page).to have_link 'Clear Runner Caches' end describe 'user clicks the button' do @@ -552,19 +567,31 @@ describe 'Pipelines', :js do end it 'increments jobs_cache_index' do - click_link 'Clear runner caches' + click_link 'Clear Runner Caches' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' end end context 'when project does not have jobs_cache_index' do it 'sets jobs_cache_index to 1' do - click_link 'Clear runner caches' + click_link 'Clear Runner Caches' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' end end end end + + describe 'Empty State' do + let(:project) { create(:project, :repository) } + + before do + visit project_pipelines_path(project) + end + + it 'renders empty state' do + expect(page).to have_content 'Build with confidence' + end + end end context 'when user is not logged in' do @@ -575,7 +602,9 @@ describe 'Pipelines', :js do context 'when project is public' do let(:project) { create(:project, :public, :repository) } - it { expect(page).to have_content 'Build with confidence' } + context 'without pipelines' do + it { expect(page).to have_content 'This project is not currently set up to run pipelines.' } + end end context 'when project is private' do diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index 2709047b8de..0a4f57bcd21 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -39,7 +39,7 @@ describe 'User manages project members' do click_link('Import') end - select(project2.name_with_namespace, from: 'source_project_id') + select(project2.full_name, from: 'source_project_id') click_button('Import') project_member = project.project_members.find_by(user_id: user_mike.id) diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb deleted file mode 100644 index 0c67196f53e..00000000000 --- a/spec/features/projects/tree/create_directory_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'spec_helper' - -feature 'Multi-file editor new directory', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - before do - project.add_master(user) - sign_in(user) - - set_cookie('new_repo', 'true') - - visit project_tree_path(project, :master) - - wait_for_requests - - click_link('Web IDE') - - wait_for_requests - end - - after do - set_cookie('new_repo', 'false') - end - - it 'creates directory in current directory' do - find('.add-to-tree').click - - click_link('New directory') - - page.within('.modal') do - find('.form-control').set('folder name') - - click_button('Create directory') - end - - find('.add-to-tree').click - - click_link('New file') - - page.within('.modal-dialog') do - find('.form-control').set('file name') - - click_button('Create file') - end - - wait_for_requests - - find('.multi-file-commit-panel-collapse-btn').click - - fill_in('commit-message', with: 'commit message ide') - - click_button('Commit') - - expect(page).to have_content('folder name') - end -end diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb deleted file mode 100644 index 85f7318c05d..00000000000 --- a/spec/features/projects/tree/create_file_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' - -feature 'Multi-file editor new file', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - before do - project.add_master(user) - sign_in(user) - - set_cookie('new_repo', 'true') - - visit project_tree_path(project, :master) - - wait_for_requests - - click_link('Web IDE') - - wait_for_requests - end - - after do - set_cookie('new_repo', 'false') - end - - it 'creates file in current directory' do - find('.add-to-tree').click - - click_link('New file') - - page.within('.modal') do - find('.form-control').set('file name') - - click_button('Create file') - end - - wait_for_requests - - find('.multi-file-commit-panel-collapse-btn').click - - fill_in('commit-message', with: 'commit message ide') - - click_button('Commit') - - expect(page).to have_content('file name') - end -end diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb deleted file mode 100644 index f81e8677e92..00000000000 --- a/spec/features/projects/tree/upload_file_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'spec_helper' - -feature 'Multi-file editor upload file', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') } - let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') } - - before do - project.add_master(user) - sign_in(user) - - set_cookie('new_repo', 'true') - - visit project_tree_path(project, :master) - - wait_for_requests - - click_link('Web IDE') - - wait_for_requests - end - - after do - set_cookie('new_repo', 'false') - 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 - - it 'uploads image 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', img_file) - - find('.add-to-tree').click - - expect(page).to have_selector('.multi-file-tab', text: 'dk.png') - expect(page).not_to have_selector('.monaco-editor') - end -end diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 77212fb105b..9e089c5a6cb 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -35,7 +35,7 @@ describe 'User searches for code' do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: 'rspec') diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index ef9553f2a91..d6120ff8517 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -34,7 +34,7 @@ describe 'User searches for issues', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: issue1.title) diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index 3b6739aecbd..68e2f7a857d 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -33,7 +33,7 @@ describe 'User searches for merge requests', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: merge_request1.title) diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index 6e197aee498..fc6cd81eb68 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -33,7 +33,7 @@ describe 'User searches for milestones', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: milestone1.title) diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 00af625dc86..7934779058f 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -18,7 +18,7 @@ describe 'User searches for wiki pages', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: 'content') diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb index aa883c964d2..66afe163447 100644 --- a/spec/features/search/user_uses_search_filters_spec.rb +++ b/spec/features/search/user_uses_search_filters_spec.rb @@ -31,7 +31,7 @@ describe 'User uses search filters', :js do wait_for_requests - expect(page).to have_link(group_project.name_with_namespace) + expect(page).to have_link(group_project.full_name) end end end @@ -43,10 +43,10 @@ describe 'User uses search filters', :js do wait_for_requests - click_link(project.name_with_namespace) + click_link(project.full_name) end - expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace) + expect(find('.js-search-project-dropdown')).to have_content(project.full_name) end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index abb7631d7d7..45439640ea3 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,9 +10,9 @@ describe IssuesFinder do set(:project3) { create(:project, group: subgroup) } set(:milestone) { create(:milestone, project: project1) } set(:label) { create(:label, project: project2) } - set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) } - set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') } - set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) } + set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) } + set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) } + set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) } set(:issue4) { create(:issue, project: project3) } set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) } set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) } @@ -275,12 +275,46 @@ describe IssuesFinder do end context 'through created_before' do - let(:params) { { created_before: issue1.created_at + 1.second } } + let(:params) { { created_before: issue1.created_at } } it 'returns issues created on or before the given date' do expect(issues).to contain_exactly(issue1) end end + + context 'through created_after and created_before' do + let(:params) { { created_after: issue2.created_at, created_before: issue3.created_at } } + + it 'returns issues created between the given dates' do + expect(issues).to contain_exactly(issue2, issue3) + end + end + end + + context 'filtering by updated_at' do + context 'through updated_after' do + let(:params) { { updated_after: issue3.updated_at } } + + it 'returns issues updated on or after the given date' do + expect(issues).to contain_exactly(issue3) + end + end + + context 'through updated_before' do + let(:params) { { updated_before: issue1.updated_at } } + + it 'returns issues updated on or before the given date' do + expect(issues).to contain_exactly(issue1) + end + end + + context 'through updated_after and updated_before' do + let(:params) { { updated_after: issue2.updated_at, updated_before: issue3.updated_at } } + + it 'returns issues updated between the given dates' do + expect(issues).to contain_exactly(issue2, issue3) + end + end end context 'filtering by reaction name' do diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 7917a00fc50..c8a43ddf410 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -109,7 +109,7 @@ describe MergeRequestsFinder do end end - context 'with created_after and created_before params' do + context 'filtering by created_at/updated_at' do let(:new_project) { create(:project, forked_from_project: project1) } let!(:new_merge_request) do @@ -117,15 +117,18 @@ describe MergeRequestsFinder do :simple, author: user, created_at: 1.week.from_now, + updated_at: 1.week.from_now, source_project: new_project, - target_project: project1) + target_project: new_project) end let!(:old_merge_request) do create(:merge_request, :simple, author: user, + source_branch: 'feature_1', created_at: 1.week.ago, + updated_at: 1.week.ago, source_project: new_project, target_project: new_project) end @@ -135,7 +138,7 @@ describe MergeRequestsFinder do end it 'filters by created_after' do - params = { project_id: project1.id, created_after: new_merge_request.created_at } + params = { project_id: new_project.id, created_after: new_merge_request.created_at } merge_requests = described_class.new(user, params).execute @@ -143,12 +146,52 @@ describe MergeRequestsFinder do end it 'filters by created_before' do - params = { project_id: new_project.id, created_before: old_merge_request.created_at + 1.second } + params = { project_id: new_project.id, created_before: old_merge_request.created_at } merge_requests = described_class.new(user, params).execute expect(merge_requests).to contain_exactly(old_merge_request) end + + it 'filters by created_after and created_before' do + params = { + project_id: new_project.id, + created_after: old_merge_request.created_at, + created_before: new_merge_request.created_at + } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + end + + it 'filters by updated_after' do + params = { project_id: new_project.id, updated_after: new_merge_request.updated_at } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(new_merge_request) + end + + it 'filters by updated_before' do + params = { project_id: new_project.id, updated_before: old_merge_request.updated_at } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request) + end + + it 'filters by updated_after and updated_before' do + params = { + project_id: new_project.id, + updated_after: old_merge_request.updated_at, + updated_before: new_merge_request.updated_at + } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 7b43494eea2..f1ae2c7ab65 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -75,6 +75,18 @@ describe NotesFinder do end end + context 'for target type' do + let(:project) { create(:project, :repository) } + let!(:note1) { create :note_on_issue, project: project } + let!(:note2) { create :note_on_commit, project: project } + + it 'finds only notes for the selected type' do + notes = described_class.new(project, user, target_type: 'issue').execute + + expect(notes).to eq([note1]) + end + end + context 'for target' do let(:project) { create(:project, :repository) } let(:note1) { create :note_on_commit, project: project } diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index 90eb0fe21e4..9747b9402a7 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -2,12 +2,13 @@ require 'spec_helper' describe TodosFinder do describe '#execute' do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:finder) { described_class } + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:finder) { described_class } before do - project.add_developer(user) + group.add_developer(user) end describe '#sort' do @@ -34,17 +35,20 @@ describe TodosFinder do end it "sorts by priority" do + project_2 = create(:project) + label_1 = create(:label, title: 'label_1', project: project, priority: 1) label_2 = create(:label, title: 'label_2', project: project, priority: 2) label_3 = create(:label, title: 'label_3', project: project, priority: 3) + label_1_2 = create(:label, title: 'label_1', project: project_2, priority: 1) issue_1 = create(:issue, title: 'issue_1', project: project) issue_2 = create(:issue, title: 'issue_2', project: project) issue_3 = create(:issue, title: 'issue_3', project: project) issue_4 = create(:issue, title: 'issue_4', project: project) - merge_request_1 = create(:merge_request, source_project: project) + merge_request_1 = create(:merge_request, source_project: project_2) - merge_request_1.labels << label_1 + merge_request_1.labels << label_1_2 # Covers the case where Todo has more than one label issue_3.labels << label_1 @@ -57,15 +61,14 @@ describe TodosFinder do todo_2 = create(:todo, user: user, project: project, target: issue_2) todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago) todo_4 = create(:todo, user: user, project: project, target: issue_1) - todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) + todo_5 = create(:todo, user: user, project: project_2, target: merge_request_1, created_at: 1.hour.ago) + + project_2.add_developer(user) todos = finder.new(user, { sort: 'priority' }).execute - expect(todos.first).to eq(todo_3) - expect(todos.second).to eq(todo_5) - expect(todos.third).to eq(todo_4) - expect(todos.fourth).to eq(todo_2) - expect(todos.fifth).to eq(todo_1) + puts todos.to_sql + expect(todos).to eq([todo_3, todo_5, todo_4, todo_2, todo_1]) end end end diff --git a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json new file mode 100644 index 00000000000..d24a6f93f4b --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "allOf": [ + { "$ref": "identity.json" }, + { + "required": [ + "export_status" + ], + "properties": { + "export_status": { + "type": "string", + "enum": ["none", "started", "finished"] + } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/public_api/v4/project/identity.json b/spec/fixtures/api/schemas/public_api/v4/project/identity.json new file mode 100644 index 00000000000..e35ab023d44 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/project/identity.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "required": [ + "id", + "description", + "name", + "name_with_namespace", + "path", + "path_with_namespace", + "created_at" + ], + "properties": { + "id": { "type": "integer" }, + "description": { "type": ["string", "null"] }, + "name": { "type": "string" }, + "name_with_namespace": { "type": "string" }, + "path": { "type": "string" }, + "path_with_namespace": { "type": "string" }, + "created_at": { "type": "date" } + } +} diff --git a/spec/fixtures/emails/update_commands_only_reply.eml b/spec/fixtures/emails/update_commands_only_reply.eml new file mode 100644 index 00000000000..bb0d2b0e03a --- /dev/null +++ b/spec/fixtures/emails/update_commands_only_reply.eml @@ -0,0 +1,38 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +In-Reply-To: <issue_1@localhost> +References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost> +Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +/close + +On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta +<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote: +> +> +> +> eviltrout posted in 'Adventure Time Sux' on Discourse Meta: +> +> --- +> hey guys everyone knows adventure time sucks! +> +> --- +> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 +> +> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). +> diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 45ffbeb27a4..4590904c93d 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -12,10 +12,10 @@ describe MembersHelper do let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } } let(:group_member_request) { group.request_access(requester) } - it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" } - it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" } - it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" } - it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" } + it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.full_name} project?" } + it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" } + it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" } + it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" } it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" } it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" } it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" } @@ -42,7 +42,7 @@ describe MembersHelper do let(:group) { build_stubbed(:group) } let(:user) { build_stubbed(:user) } - it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" } + it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.full_name}\" project?" } it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" } end end diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb index f55163c26e9..63806ef91f3 100644 --- a/spec/helpers/todos_helper_spec.rb +++ b/spec/helpers/todos_helper_spec.rb @@ -26,8 +26,8 @@ describe TodosHelper do expected_results = [ { 'id' => '', 'text' => 'Any Project' }, - { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace }, - { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace } + { 'id' => projects.second.id, 'text' => projects.second.full_name }, + { 'id' => projects.first.id, 'text' => projects.first.full_name } ] expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results) diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 80a598e63bd..13d607a06d2 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -9,8 +9,8 @@ import axios from '~/lib/utils/axios_utils'; import '~/boards/models/assignee'; import eventHub from '~/boards/eventhub'; +import '~/vue_shared/models/label'; import '~/boards/models/list'; -import '~/boards/models/label'; import '~/boards/stores/boards_store'; import boardCard from '~/boards/components/board_card.vue'; import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 8411f4dd8a6..0cf9e4c9ba1 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -7,8 +7,8 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Cookies from 'js-cookie'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 278155c585e..37088a6421c 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -4,8 +4,8 @@ import Vue from 'vue'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/stores/boards_store'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index dbbe14fe3e0..4a11131b55c 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -3,8 +3,8 @@ /* global ListIssue */ import Vue from 'vue'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 34964b20b05..d9a1d692949 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -6,8 +6,8 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import _ from 'underscore'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index 7eecb58a4c3..e9d77f035e3 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -1,7 +1,7 @@ /* global ListIssue */ +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/stores/modal_store'; diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index f1da5f81c0f..756a654765b 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -128,6 +128,24 @@ describe('Filtered Search Visual Tokens', () => { }); }); + describe('getEndpointWithQueryParams', () => { + it('returns `endpoint` string as is when second param `endpointQueryParams` is undefined, null or empty string', () => { + const endpoint = 'foo/bar/labels.json'; + expect(subject.getEndpointWithQueryParams(endpoint)).toBe(endpoint); + expect(subject.getEndpointWithQueryParams(endpoint, null)).toBe(endpoint); + expect(subject.getEndpointWithQueryParams(endpoint, '')).toBe(endpoint); + }); + + it('returns `endpoint` string with values of `endpointQueryParams`', () => { + const endpoint = 'foo/bar/labels.json'; + const singleQueryParams = '{"foo":"true"}'; + const multipleQueryParams = '{"foo":"true","bar":"true"}'; + + expect(subject.getEndpointWithQueryParams(endpoint, singleQueryParams)).toBe(`${endpoint}?foo=true`); + expect(subject.getEndpointWithQueryParams(endpoint, multipleQueryParams)).toBe(`${endpoint}?foo=true&bar=true`); + }); + }); + describe('unselectTokens', () => { it('does nothing when there are no tokens', () => { const beforeHTML = tokensContainer.innerHTML; diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 3adc29262f3..46c7b9f54f2 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -129,7 +129,7 @@ describe('AppComponent', () => { vm.fetchGroups({}); setTimeout(() => { - expect(vm.isLoading).toBeFalsy(); + expect(vm.isLoading).toBe(false); expect($.scrollTo).toHaveBeenCalledWith(0); expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.'); done(); @@ -144,10 +144,10 @@ describe('AppComponent', () => { spyOn(vm, 'updateGroups').and.callThrough(); vm.fetchAllGroups(); - expect(vm.isLoading).toBeTruthy(); + expect(vm.isLoading).toBe(true); expect(vm.fetchGroups).toHaveBeenCalled(); setTimeout(() => { - expect(vm.isLoading).toBeFalsy(); + expect(vm.isLoading).toBe(false); expect(vm.updateGroups).toHaveBeenCalled(); done(); }, 0); @@ -181,7 +181,7 @@ describe('AppComponent', () => { spyOn($, 'scrollTo'); vm.fetchPage(2, null, null, true); - expect(vm.isLoading).toBeTruthy(); + expect(vm.isLoading).toBe(true); expect(vm.fetchGroups).toHaveBeenCalledWith({ page: 2, filterGroupsBy: null, @@ -190,7 +190,7 @@ describe('AppComponent', () => { archived: true, }); setTimeout(() => { - expect(vm.isLoading).toBeFalsy(); + expect(vm.isLoading).toBe(false); expect($.scrollTo).toHaveBeenCalledWith(0); expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); expect(window.history.replaceState).toHaveBeenCalledWith({ @@ -216,7 +216,7 @@ describe('AppComponent', () => { spyOn(vm.store, 'setGroupChildren'); vm.toggleChildren(groupItem); - expect(groupItem.isChildrenLoading).toBeTruthy(); + expect(groupItem.isChildrenLoading).toBe(true); expect(vm.fetchGroups).toHaveBeenCalledWith({ parentId: groupItem.id, }); @@ -232,7 +232,7 @@ describe('AppComponent', () => { vm.toggleChildren(groupItem); expect(vm.fetchGroups).not.toHaveBeenCalled(); - expect(groupItem.isOpen).toBeTruthy(); + expect(groupItem.isOpen).toBe(true); }); it('should collapse group if it is already expanded', () => { @@ -241,16 +241,16 @@ describe('AppComponent', () => { vm.toggleChildren(groupItem); expect(vm.fetchGroups).not.toHaveBeenCalled(); - expect(groupItem.isOpen).toBeFalsy(); + expect(groupItem.isOpen).toBe(false); }); it('should set `isChildrenLoading` back to `false` if load request fails', (done) => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true)); vm.toggleChildren(groupItem); - expect(groupItem.isChildrenLoading).toBeTruthy(); + expect(groupItem.isChildrenLoading).toBe(true); setTimeout(() => { - expect(groupItem.isChildrenLoading).toBeFalsy(); + expect(groupItem.isChildrenLoading).toBe(false); done(); }, 0); }); @@ -268,10 +268,10 @@ describe('AppComponent', () => { it('updates props which show modal confirmation dialog', () => { const group = Object.assign({}, mockParentGroupItem); - expect(vm.updateModal).toBeFalsy(); + expect(vm.showModal).toBe(false); expect(vm.groupLeaveConfirmationMessage).toBe(''); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.updateModal).toBeTruthy(); + expect(vm.showModal).toBe(true); expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); }); }); @@ -280,9 +280,9 @@ describe('AppComponent', () => { it('hides modal confirmation which is shown before leaving the group', () => { const group = Object.assign({}, mockParentGroupItem); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.updateModal).toBeTruthy(); + expect(vm.showModal).toBe(true); vm.hideLeaveGroupModal(); - expect(vm.updateModal).toBeFalsy(); + expect(vm.showModal).toBe(false); }); }); @@ -307,8 +307,8 @@ describe('AppComponent', () => { spyOn($, 'scrollTo'); vm.leaveGroup(); - expect(vm.updateModal).toBeFalsy(); - expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.showModal).toBe(false); + expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { expect($.scrollTo).toHaveBeenCalledWith(0); @@ -325,12 +325,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(); - expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBe(false); done(); }, 0); }); @@ -342,12 +342,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(childGroupItem, groupItem); - expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBe(false); done(); }, 0); }); @@ -379,10 +379,10 @@ describe('AppComponent', () => { it('should set `isSearchEmpty` prop based on groups count', () => { vm.updateGroups(mockGroups); - expect(vm.isSearchEmpty).toBeFalsy(); + expect(vm.isSearchEmpty).toBe(false); vm.updateGroups([]); - expect(vm.isSearchEmpty).toBeTruthy(); + expect(vm.isSearchEmpty).toBe(true); }); }); }); @@ -473,13 +473,16 @@ describe('AppComponent', () => { }); }); - it('renders modal confirmation dialog', () => { + it('renders modal confirmation dialog', (done) => { vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; - vm.updateModal = true; - const modalDialogEl = vm.$el.querySelector('.modal'); - expect(modalDialogEl).not.toBe(null); - expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); - expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + vm.showModal = true; + Vue.nextTick(() => { + const modalDialogEl = vm.$el.querySelector('.modal'); + expect(modalDialogEl).not.toBe(null); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + done(); + }); }); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 49799c31995..27f06573432 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -166,6 +166,21 @@ describe('common_utils', () => { }); }); + describe('objectToQueryString', () => { + it('returns empty string when `param` is undefined, null or empty string', () => { + expect(commonUtils.objectToQueryString()).toBe(''); + expect(commonUtils.objectToQueryString('')).toBe(''); + }); + + it('returns query string with values of `params`', () => { + const singleQueryParams = { foo: true }; + const multipleQueryParams = { foo: true, bar: true }; + + expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true'); + expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true'); + }); + }); + describe('buildUrlWithCurrentLocation', () => { it('should build an url with current location and given parameters', () => { expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); diff --git a/spec/javascripts/pipelines/blank_state_spec.js b/spec/javascripts/pipelines/blank_state_spec.js new file mode 100644 index 00000000000..b7a9b60d85c --- /dev/null +++ b/spec/javascripts/pipelines/blank_state_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import component from '~/pipelines/components/blank_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Pipelines Blank State', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(component); + + vm = mountComponent(Component, + { + svgPath: 'foo', + message: 'Blank State', + }, + ); + }); + + it('should render svg', () => { + expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo'); + }); + + it('should render message', () => { + expect( + vm.$el.querySelector('h4').textContent.trim(), + ).toEqual('Blank State'); + }); +}); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 97f04844b3a..71f77e5f42e 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import emptyStateComp from '~/pipelines/components/empty_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Empty State', () => { let component; @@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => { beforeEach(() => { EmptyStateComponent = Vue.extend(emptyStateComp); - component = new EmptyStateComponent({ - propsData: { - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - }, - }).$mount(); + component = mountComponent(EmptyStateComponent, { + helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + canSetCi: true, + }); + }); + + afterEach(() => { + component.$destroy(); }); it('should render empty state SVG', () => { @@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( - component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), - ).toContain('Continous Integration can help catch bugs by running your tests automatically'); + component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), + ).toContain('Continous Integration can help catch bugs by running your tests automatically,'); expect( - component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), - ).toContain('Continuous Deployment can help you deliver code to your product environment'); + component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), + ).toContain('while Continuous Deployment can help you deliver code to your product environment'); }); it('should render a link with provided help path', () => { - expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); - expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); + expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines'); }); }); diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js deleted file mode 100644 index a402857a4d1..00000000000 --- a/spec/javascripts/pipelines/error_state_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import Vue from 'vue'; -import errorStateComp from '~/pipelines/components/error_state.vue'; - -describe('Pipelines Error State', () => { - let component; - let ErrorStateComponent; - - beforeEach(() => { - ErrorStateComponent = Vue.extend(errorStateComp); - - component = new ErrorStateComponent({ - propsData: { - errorStateSvgPath: 'foo', - }, - }).$mount(); - }); - - it('should render error state SVG', () => { - expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); - }); - - it('should render emtpy state information', () => { - expect( - component.$el.querySelector('h4').textContent, - ).toContain('The API failed to fetch the pipelines'); - }); -}); diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index 09a0c14d96c..77c5258f74c 100644 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -1,116 +1,68 @@ import Vue from 'vue'; import navControlsComp from '~/pipelines/components/nav_controls.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Nav Controls', () => { let NavControlsComponent; + let component; beforeEach(() => { NavControlsComponent = Vue.extend(navControlsComp); }); + afterEach(() => { + component.$destroy(); + }); + it('should render link to create a new pipeline', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: true, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); - expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); + expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline'); + expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(mockData.newPipelinePath); }); - it('should not render link to create pipeline if no permission is provided', () => { + it('should not render link to create pipeline if no path is provided', () => { const mockData = { - newPipelinePath: 'foo', - hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: false, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelector('.btn-create')).toEqual(null); + expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null); }); it('should render link for resetting runner caches', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: false, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); - expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); + expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches'); + expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath); }); it('should render link for CI lint', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - canCreatePipeline: true, - }; - - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); - - expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint'); - expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath); - }); - - it('should render link to help page when CI is not enabled', () => { - const mockData = { - newPipelinePath: 'foo', - hasCiEnabled: false, - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - canCreatePipeline: true, - }; - - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); - - expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); - expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath); - }); - - it('should not render link to help page when CI is enabled', () => { - const mockData = { - newPipelinePath: 'foo', - hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: true, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelector('.btn-info')).toEqual(null); + expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint'); + expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(mockData.ciLintPath); }); }); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 54d5bfd51e6..84fd0329f08 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; - preloadFixtures('static/pipelines.html.raw'); preloadFixtures(jsonFixtureName); let PipelinesComponent; let pipelines; - let component; + let vm; + const paths = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + ciLintPath: '/ci/lint', + resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache', + newPipelinePath: '/twitter/flight/pipelines/new', + }; + + const noPermissions = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + }; beforeEach(() => { - loadFixtures('static/pipelines.html.raw'); pipelines = getJSONFixture(jsonFixtureName); PipelinesComponent = Vue.extend(pipelinesComp); }); afterEach(() => { - component.$destroy(); + vm.$destroy(); + }); + + const pipelinesInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(pipelines), { + status: 200, + })); + }; + + const emptyStateInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }), { + status: 200, + })); + }; + + const errorInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({}), { + status: 500, + })); + }; + + describe('With permission', () => { + describe('With pipelines in main tab', () => { + beforeEach((done) => { + Vue.http.interceptors.push(pipelinesInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('renders Run Pipeline button', () => { + expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath); + }); + + it('renders CI Lint button', () => { + expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath); + }); + + it('renders pipelines table', () => { + expect( + vm.$el.querySelectorAll('.gl-responsive-table-row').length, + ).toEqual(pipelines.pipelines.length + 1); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('renders Run Pipeline button', () => { + expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath); + }); + + it('renders CI Lint button', () => { + expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath); + }); + + it('renders tab empty state', () => { + expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + + it('renders empty state', () => { + expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence'); + expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath); + }); + + it('does not render tabs nor buttons', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull(); + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + }); + + describe('When API returns error', () => { + beforeEach((done) => { + Vue.http.interceptors.push(errorInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, errorInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('renders buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath); + expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); + expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath); + }); + + it('renders error state', () => { + expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.'); + }); + }); + }); + + describe('Without permission', () => { + describe('With pipelines in main tab', () => { + beforeEach((done) => { + Vue.http.interceptors.push(pipelinesInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: false, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + + it('renders pipelines table', () => { + expect( + vm.$el.querySelectorAll('.gl-responsive-table-row').length, + ).toEqual(pipelines.pipelines.length + 1); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: false, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + + it('renders tab empty state', () => { + expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: false, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + + it('renders empty state without button to set CI', () => { + expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.'); + expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull(); + }); + + it('does not render tabs or buttons', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull(); + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + }); + + describe('When API returns error', () => { + beforeEach((done) => { + Vue.http.interceptors.push(errorInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: true, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, errorInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('does not renders buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + + it('renders error state', () => { + expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.'); + }); + }); }); describe('successfull request', () => { describe('with pipelines', () => { - const pipelinesInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(pipelines), { - status: 200, - })); - }; - beforeEach(() => { Vue.http.interceptors.push(pipelinesInterceptor); - component = mountComponent(PipelinesComponent, { + vm = mountComponent(PipelinesComponent, { store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, }); }); @@ -48,9 +392,9 @@ describe('Pipelines', () => { it('should render table', (done) => { setTimeout(() => { - expect(component.$el.querySelector('.table-holder')).toBeDefined(); + expect(vm.$el.querySelector('.table-holder')).toBeDefined(); expect( - component.$el.querySelectorAll('.gl-responsive-table-row').length, + vm.$el.querySelectorAll('.gl-responsive-table-row').length, ).toEqual(pipelines.pipelines.length + 1); done(); }); @@ -59,22 +403,22 @@ describe('Pipelines', () => { it('should render navigation tabs', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), ).toContain('Pending'); expect( - component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), ).toContain('All'); expect( - component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), ).toContain('Running'); expect( - component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), ).toContain('Finished'); expect( - component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), ).toContain('Branches'); expect( - component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), ).toContain('Tags'); done(); }); @@ -82,10 +426,10 @@ describe('Pipelines', () => { it('should make an API request when using tabs', (done) => { setTimeout(() => { - spyOn(component, 'updateContent'); - component.$el.querySelector('.js-pipelines-tab-finished').click(); + spyOn(vm, 'updateContent'); + vm.$el.querySelector('.js-pipelines-tab-finished').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); + expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); done(); }); }); @@ -93,9 +437,9 @@ describe('Pipelines', () => { describe('with pagination', () => { it('should make an API request when using pagination', (done) => { setTimeout(() => { - spyOn(component, 'updateContent'); + spyOn(vm, 'updateContent'); // Mock pagination - component.store.state.pageInfo = { + vm.store.state.pageInfo = { page: 1, total: 10, perPage: 2, @@ -103,9 +447,9 @@ describe('Pipelines', () => { totalPages: 5, }; - Vue.nextTick(() => { - component.$el.querySelector('.js-next-button a').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + vm.$nextTick(() => { + vm.$el.querySelector('.js-next-button a').click(); + expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); done(); }); @@ -113,112 +457,249 @@ describe('Pipelines', () => { }); }); }); + }); - describe('without pipelines', () => { - const emptyInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); - }; + describe('methods', () => { + beforeEach(() => { + spyOn(history, 'pushState').and.stub(); + }); - beforeEach(() => { - Vue.http.interceptors.push(emptyInterceptor); - }); + describe('updateContent', () => { + it('should set given parameters', () => { + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + vm.updateContent({ scope: 'finished', page: '4' }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, emptyInterceptor, - ); + expect(vm.page).toEqual('4'); + expect(vm.scope).toEqual('finished'); + expect(vm.requestData.scope).toEqual('finished'); + expect(vm.requestData.page).toEqual('4'); }); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + spyOn(vm, 'updateContent'); - it('should render empty state', (done) => { - component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); + vm.onChangeTab('running'); - setTimeout(() => { - expect(component.$el.querySelector('.empty-state')).not.toBe(null); - done(); + expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, }); + spyOn(vm, 'updateContent'); + + vm.onChangePage(4); + + expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' }); }); }); }); - describe('unsuccessfull request', () => { - const errorInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 500, - })); - }; - + describe('computed properties', () => { beforeEach(() => { - Vue.http.interceptors.push(errorInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, errorInterceptor, - ); + describe('tabs', () => { + it('returns default tabs', () => { + expect(vm.tabs).toEqual([ + { name: 'All', scope: 'all', count: undefined, isActive: true }, + { name: 'Pending', scope: 'pending', count: undefined, isActive: false }, + { name: 'Running', scope: 'running', count: undefined, isActive: false }, + { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, + { name: 'Branches', scope: 'branches', isActive: false }, + { name: 'Tags', scope: 'tags', isActive: false }, + ]); + }); }); - it('should render error state', (done) => { - component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); + describe('emptyTabMessage', () => { + it('returns message with scope', (done) => { + vm.scope = 'pending'; - setTimeout(() => { - expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); - done(); + vm.$nextTick(() => { + expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); + done(); + }); }); - }); - }); - describe('methods', () => { - beforeEach(() => { - spyOn(history, 'pushState').and.stub(); + it('returns message without scope when scope is `all`', () => { + expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.'); + }); }); - describe('updateContent', () => { - it('should set given parameters', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('stateToRender', () => { + it('returns loading state when the app is loading', () => { + expect(vm.stateToRender).toEqual('loading'); + }); + + it('returns error state when app has error', (done) => { + vm.hasError = true; + vm.isLoading = false; + + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('error'); + done(); + }); + }); + + it('returns table list when app has pipelines', (done) => { + vm.isLoading = false; + vm.hasError = false; + vm.state.pipelines = pipelines.pipelines; + + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('tableList'); + + done(); + }); + }); + + it('returns empty tab when app does not have pipelines but project has pipelines', (done) => { + vm.state.count.all = 10; + vm.isLoading = false; + + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyTab'); + + done(); }); - component.updateContent({ scope: 'finished', page: '4' }); + }); + + it('returns empty tab when project has CI', (done) => { + vm.isLoading = false; + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyTab'); - expect(component.page).toEqual('4'); - expect(component.scope).toEqual('finished'); - expect(component.requestData.scope).toEqual('finished'); - expect(component.requestData.page).toEqual('4'); + done(); + }); + }); + + it('returns empty state when project does not have pipelines nor CI', (done) => { + vm.isLoading = false; + vm.hasGitlabCi = false; + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyState'); + + done(); + }); }); }); - describe('onChangeTab', () => { - it('should set page to 1', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('shouldRenderTabs', () => { + it('returns true when state is loading & has already made the first request', (done) => { + vm.isLoading = true; + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); }); - spyOn(component, 'updateContent'); + }); - component.onChangeTab('running'); + it('returns true when state is tableList & has already made the first request', (done) => { + vm.isLoading = false; + vm.state.pipelines = pipelines.pipelines; + vm.hasMadeRequest = true; - expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); + }); + }); + + it('returns true when state is error & has already made the first request', (done) => { + vm.isLoading = false; + vm.hasError = true; + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); + }); + }); + + it('returns true when state is empty tab & has already made the first request', (done) => { + vm.isLoading = false; + vm.state.count.all = 10; + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); + }); + }); + + it('returns false when has not made first request', (done) => { + vm.hasMadeRequest = false; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(false); + + done(); + }); + }); + + it('returns false when state is emtpy state', (done) => { + vm.isLoading = false; + vm.hasMadeRequest = true; + vm.hasGitlabCi = false; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(false); + + done(); + }); }); }); - describe('onChangePage', () => { - it('should update page and keep scope', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('shouldRenderButtons', () => { + it('returns true when it has paths & has made the first request', (done) => { + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderButtons).toEqual(true); + + done(); }); - spyOn(component, 'updateContent'); + }); + + it('returns false when it has not made the first request', (done) => { + vm.hasMadeRequest = false; - component.onChangePage(4); + vm.$nextTick(() => { + expect(vm.shouldRenderButtons).toEqual(false); - expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); + done(); + }); }); }); }); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js deleted file mode 100644 index b509cedbe80..00000000000 --- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; - -describe('Multi-file editor commit sidebar list collapsed', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(listCollapsed); - - vm = createComponentWithStore(Component, store); - - vm.$store.state.openFiles.push(file('file1'), file('file2')); - vm.$store.state.openFiles[0].tempFile = true; - vm.$store.state.openFiles.forEach((f) => { - Object.assign(f, { - changed: true, - }); - }); - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders added & modified files count', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1'); - }); -}); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js deleted file mode 100644 index 6f1a1d874d3..00000000000 --- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import Vue from 'vue'; -import listItem from '~/ide/components/commit_sidebar/list_item.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; - -describe('Multi-file editor commit sidebar list item', () => { - let vm; - let f; - - beforeEach(() => { - const Component = Vue.extend(listItem); - - f = file('test-file'); - - vm = mountComponent(Component, { - file: f, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders file path', () => { - expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); - }); - - describe('computed', () => { - describe('iconName', () => { - it('returns modified when not a tempFile', () => { - expect(vm.iconName).toBe('file-modified'); - }); - - it('returns addition when not a tempFile', () => { - f.tempFile = true; - - expect(vm.iconName).toBe('file-addition'); - }); - }); - - describe('iconClass', () => { - it('returns modified when not a tempFile', () => { - expect(vm.iconClass).toContain('multi-file-modified'); - }); - - it('returns addition when not a tempFile', () => { - f.tempFile = true; - - expect(vm.iconClass).toContain('multi-file-addition'); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js deleted file mode 100644 index aeb9de9ace4..00000000000 --- a/spec/javascripts/repo/components/commit_sidebar/list_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; - -describe('Multi-file editor commit sidebar list', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(commitSidebarList); - - vm = createComponentWithStore(Component, store, { - title: 'Staged', - fileList: [], - }); - - vm.$store.state.rightPanelCollapsed = false; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('empty file list', () => { - it('renders no changes text', () => { - expect(vm.$el.querySelector('.help-block').textContent.trim()).toBe('No changes'); - }); - }); - - describe('with a list of files', () => { - beforeEach((done) => { - const f = file('file name'); - f.changed = true; - vm.fileList.push(f); - - Vue.nextTick(done); - }); - - it('renders list', () => { - expect(vm.$el.querySelectorAll('li').length).toBe(1); - }); - }); - - describe('collapsed', () => { - beforeEach((done) => { - vm.$store.state.rightPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('hides list', () => { - expect(vm.$el.querySelector('.list-unstyled')).toBeNull(); - expect(vm.$el.querySelector('.help-block')).toBeNull(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_context_bar_spec.js b/spec/javascripts/repo/components/ide_context_bar_spec.js deleted file mode 100644 index 935da259a99..00000000000 --- a/spec/javascripts/repo/components/ide_context_bar_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ideContextBar from '~/ide/components/ide_context_bar.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; - -describe('Multi-file editor right context bar', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(ideContextBar); - - vm = createComponentWithStore(Component, store); - - vm.$store.state.rightPanelCollapsed = false; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('collapsed', () => { - beforeEach((done) => { - vm.$store.state.rightPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('adds collapsed class', () => { - expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); - }); - - it('shows correct icon', () => { - expect(vm.currentIcon).toBe('angle-double-left'); - }); - }); - - it('clicking toggle collapse button collapses the bar', () => { - spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve()); - - vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); - - expect(vm.setPanelCollapsedStatus).toHaveBeenCalledWith({ - side: 'right', - collapsed: true, - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_repo_tree_spec.js b/spec/javascripts/repo/components/ide_repo_tree_spec.js deleted file mode 100644 index e3bbda514da..00000000000 --- a/spec/javascripts/repo/components/ide_repo_tree_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ideRepoTree from '~/ide/components/ide_repo_tree.vue'; -import { file, resetStore } from '../helpers'; - -describe('IdeRepoTree', () => { - let vm; - - beforeEach(() => { - const IdeRepoTree = Vue.extend(ideRepoTree); - - vm = new IdeRepoTree({ - store, - propsData: { - treeId: 'abcproject/mybranch', - }, - }); - - vm.$store.state.currentBranch = 'master'; - vm.$store.state.isRoot = true; - vm.$store.state.trees['abcproject/mybranch'] = { - tree: [file()], - }; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a sidebar', () => { - const tbody = vm.$el.querySelector('tbody'); - - expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); - expect(tbody.querySelector('.repo-file-options')).toBeFalsy(); - expect(tbody.querySelector('.prev-directory')).toBeFalsy(); - expect(tbody.querySelector('.loading-file')).toBeFalsy(); - expect(tbody.querySelector('.file')).toBeTruthy(); - }); - - it('renders 3 loading files if tree is loading', (done) => { - vm.treeId = '123'; - - Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3); - - done(); - }); - }); - - it('renders a prev directory if is not root', (done) => { - vm.$store.state.isRoot = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_side_bar_spec.js b/spec/javascripts/repo/components/ide_side_bar_spec.js deleted file mode 100644 index 79c3c8128e8..00000000000 --- a/spec/javascripts/repo/components/ide_side_bar_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ideSidebar from '~/ide/components/ide_side_bar.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../helpers'; - -describe('IdeSidebar', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(ideSidebar); - - vm = createComponentWithStore(Component, store).$mount(); - - vm.$store.state.leftPanelCollapsed = false; - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a sidebar', () => { - expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull(); - }); - - describe('collapsed', () => { - beforeEach((done) => { - vm.$store.state.leftPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('adds collapsed class', () => { - expect(vm.$el.classList).toContain('is-collapsed'); - }); - - it('shows correct icon', () => { - expect(vm.currentIcon).toBe('angle-double-right'); - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_spec.js b/spec/javascripts/repo/components/ide_spec.js deleted file mode 100644 index 18135177b5e..00000000000 --- a/spec/javascripts/repo/components/ide_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ide from '~/ide/components/ide.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file, resetStore } from '../helpers'; - -describe('ide component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(ide); - - vm = createComponentWithStore(Component, store, { - emptyStateSvgPath: 'svg', - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('does not render panel right when no files open', () => { - expect(vm.$el.querySelector('.panel-right')).toBeNull(); - }); - - it('renders panel right when files are open', (done) => { - vm.$store.state.trees['abcproject/mybranch'] = { - tree: [file()], - }; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.panel-right')).toBeNull(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js deleted file mode 100644 index 82597fc75e8..00000000000 --- a/spec/javascripts/repo/components/new_branch_form_spec.js +++ /dev/null @@ -1,114 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import newBranchForm from '~/ide/components/new_branch_form.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../helpers'; - -describe('Multi-file editor new branch form', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(newBranchForm); - - vm = createComponentWithStore(Component, store); - - vm.$store.state.currentBranch = 'master'; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - describe('template', () => { - it('renders submit as disabled', () => { - expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBe('disabled'); - }); - - it('enables the submit button when branch is not empty', (done) => { - vm.branchName = 'testing'; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBeNull(); - - done(); - }); - }); - - it('displays current branch creating from', (done) => { - Vue.nextTick(() => { - expect(vm.$el.querySelector('p').textContent.replace(/\s+/g, ' ').trim()).toBe('Create from: master'); - - done(); - }); - }); - }); - - describe('submitNewBranch', () => { - beforeEach(() => { - spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve()); - }); - - it('sets to loading', () => { - vm.submitNewBranch(); - - expect(vm.loading).toBeTruthy(); - }); - - it('hides current flash element', (done) => { - vm.$refs.flashContainer.innerHTML = '<div class="flash-alert"></div>'; - - vm.submitNewBranch(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.flash-alert')).toBeNull(); - - done(); - }); - }); - - it('calls createdNewBranch with branchName', () => { - vm.branchName = 'testing'; - - vm.submitNewBranch(); - - expect(vm.createNewBranch).toHaveBeenCalledWith('testing'); - }); - }); - - describe('submitNewBranch with error', () => { - beforeEach(() => { - spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({ - json: () => Promise.resolve({ - message: 'error message', - }), - })); - }); - - it('sets loading to false', (done) => { - vm.loading = true; - - vm.submitNewBranch(); - - setTimeout(() => { - expect(vm.loading).toBeFalsy(); - - done(); - }); - }); - - it('creates flash element', (done) => { - vm.submitNewBranch(); - - setTimeout(() => { - expect(vm.$el.querySelector('.flash-alert')).not.toBeNull(); - expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message'); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js deleted file mode 100644 index 4a8e4445e2f..00000000000 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import newDropdown from '~/ide/components/new_dropdown/index.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; - -describe('new dropdown component', () => { - let vm; - - beforeEach(() => { - const component = Vue.extend(newDropdown); - - vm = createComponentWithStore(component, store, { - branch: 'master', - path: '', - }); - - vm.$store.state.currentProjectId = 'abcproject'; - vm.$store.state.path = ''; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - 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'); - }); - - describe('createNewItem', () => { - it('sets modalType to blob when new file is clicked', () => { - vm.$el.querySelectorAll('a')[0].click(); - - expect(vm.modalType).toBe('blob'); - }); - - 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(); - - Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.modal')).toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js deleted file mode 100644 index d6a1fdd115c..00000000000 --- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js +++ /dev/null @@ -1,237 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import modal from '~/ide/components/new_dropdown/modal.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file, resetStore } from '../../helpers'; - -describe('new file modal component', () => { - const Component = Vue.extend(modal); - let vm; - let projectTree; - - beforeEach(() => { - spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({ - data: { - id: '123', - }, - })); - - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { - id: '123branch', - }, - }, - })); - - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - ['tree', 'blob'].forEach((type) => { - describe(type, () => { - beforeEach(() => { - store.state.projects.abcproject = { - web_url: '', - }; - store.state.trees = []; - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - projectTree = store.state.trees['abcproject/mybranch']; - store.state.currentProjectId = 'abcproject'; - - vm = createComponentWithStore(Component, store, { - type, - branchId: 'master', - path: '', - parent: projectTree, - }); - - vm.entryName = 'testing'; - - vm.$mount(); - }); - - it(`sets modal title as ${type}`, () => { - const title = type === 'tree' ? 'directory' : 'file'; - - expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`); - }); - - it(`sets button label as ${type}`, () => { - const title = type === 'tree' ? 'directory' : 'file'; - - expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`); - }); - - it(`sets form label as ${type}`, () => { - const title = type === 'tree' ? 'Directory' : 'File'; - - expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`); - }); - - describe('createEntryInStore', () => { - it('calls createTempEntry', () => { - spyOn(vm, 'createTempEntry'); - - vm.createEntryInStore(); - - expect(vm.createTempEntry).toHaveBeenCalledWith({ - projectId: 'abcproject', - branchId: 'master', - parent: projectTree, - name: 'testing', - type, - }); - }); - - it('sets editMode to true', (done) => { - vm.createEntryInStore(); - - setTimeout(() => { - expect(vm.$store.state.editMode).toBeTruthy(); - - done(); - }); - }); - - it('toggles blob view', (done) => { - vm.createEntryInStore(); - - setTimeout(() => { - expect(vm.$store.state.currentBlobView).toBe('repo-editor'); - - done(); - }); - }); - - it('opens newly created file', (done) => { - if (type === 'blob') { - vm.createEntryInStore(); - - setTimeout(() => { - expect(vm.$store.state.openFiles.length).toBe(1); - expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep'); - - done(); - }); - } else { - done(); - } - }); - - if (type === 'blob') { - it('creates new file', (done) => { - vm.createEntryInStore(); - - setTimeout(() => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('testing'); - expect(baseTree[0].type).toBe('blob'); - expect(baseTree[0].tempFile).toBeTruthy(); - - done(); - }); - }); - - it('does not create temp file when file already exists', (done) => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - baseTree.push(file('testing', '1', type)); - - vm.createEntryInStore(); - - setTimeout(() => { - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('testing'); - expect(baseTree[0].type).toBe('blob'); - expect(baseTree[0].tempFile).toBeFalsy(); - - done(); - }); - }); - } else { - it('creates new tree', () => { - vm.createEntryInStore(); - - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('testing'); - expect(baseTree[0].type).toBe('tree'); - expect(baseTree[0].tempFile).toBeTruthy(); - }); - - it('creates multiple trees when entryName has slashes', () => { - vm.entryName = 'app/test'; - vm.createEntryInStore(); - - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('app'); - }); - - it('creates tree in existing tree', () => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - baseTree.push(file('app', '1', 'tree')); - - vm.entryName = 'app/test'; - vm.createEntryInStore(); - - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('app'); - expect(baseTree[0].tempFile).toBeFalsy(); - expect(baseTree[0].tree[0].tempFile).toBeTruthy(); - expect(baseTree[0].tree[0].name).toBe('test'); - }); - - it('does not create new tree when already exists', () => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - baseTree.push(file('app', '1', 'tree')); - - vm.entryName = 'app'; - vm.createEntryInStore(); - - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('app'); - expect(baseTree[0].tempFile).toBeFalsy(); - expect(baseTree[0].tree.length).toBe(0); - }); - } - }); - }); - }); - - it('focuses field on mount', () => { - document.body.innerHTML += '<div class="js-test"></div>'; - - vm = createComponentWithStore(Component, store, { - type: 'tree', - projectId: 'abcproject', - branchId: 'master', - path: '', - }).$mount('.js-test'); - - expect(document.activeElement).toBe(vm.$refs.fieldName); - - vm.$el.remove(); - }); -}); diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js deleted file mode 100644 index ee8aab3a252..00000000000 --- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js +++ /dev/null @@ -1,158 +0,0 @@ -import Vue from 'vue'; -import upload from '~/ide/components/new_dropdown/upload.vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; - -describe('new dropdown upload', () => { - let vm; - let projectTree; - - beforeEach(() => { - spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({ - data: { - id: '123', - }, - })); - - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { - id: '123branch', - }, - }, - })); - - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - - const Component = Vue.extend(upload); - - store.state.projects.abcproject = { - web_url: '', - }; - store.state.currentProjectId = 'abcproject'; - store.state.trees = []; - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - projectTree = store.state.trees['abcproject/mybranch']; - - vm = createComponentWithStore(Component, store, { - branchId: 'master', - path: '', - parent: projectTree, - }); - - vm.entryName = 'testing'; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - describe('readFile', () => { - beforeEach(() => { - spyOn(FileReader.prototype, 'readAsText'); - spyOn(FileReader.prototype, 'readAsDataURL'); - }); - - it('calls readAsText for text files', () => { - const file = { - type: 'text/html', - }; - - vm.readFile(file); - - expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file); - }); - - it('calls readAsDataURL for non-text files', () => { - const file = { - type: 'images/png', - }; - - vm.readFile(file); - - expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file); - }); - }); - - describe('createFile', () => { - const target = { - result: 'content', - }; - const binaryTarget = { - result: 'base64,base64content', - }; - const file = { - name: 'file', - }; - - it('creates new file', (done) => { - vm.createFile(target, file, true); - - vm.$nextTick(() => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe(file.name); - expect(baseTree[0].content).toBe(target.result); - - done(); - }); - }); - - it('creates new file in path', (done) => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - const tree = { - type: 'tree', - name: 'testing', - path: 'testing', - tree: [], - }; - baseTree.push(tree); - - vm.parent = tree; - vm.createFile(target, file, true); - - vm.$nextTick(() => { - expect(baseTree.length).toBe(1); - expect(baseTree[0].tree[0].name).toBe(file.name); - expect(baseTree[0].tree[0].content).toBe(target.result); - expect(baseTree[0].tree[0].path).toBe(`testing/${file.name}`); - - done(); - }); - }); - - it('splits content on base64 if binary', (done) => { - vm.createFile(binaryTarget, file, false); - - vm.$nextTick(() => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe(file.name); - expect(baseTree[0].content).toBe(binaryTarget.result.split('base64,')[1]); - expect(baseTree[0].base64).toBe(true); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js deleted file mode 100644 index 934ada9dec2..00000000000 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import Vue from 'vue'; -import * as urlUtils from '~/lib/utils/url_utility'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import repoCommitSection from '~/ide/components/repo_commit_section.vue'; -import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; -import { file, resetStore } from '../helpers'; - -describe('RepoCommitSection', () => { - let vm; - - function createComponent() { - const RepoCommitSection = Vue.extend(repoCommitSection); - - const comp = new RepoCommitSection({ - store, - }).$mount(); - - comp.$store.state.currentProjectId = 'abcproject'; - comp.$store.state.currentBranchId = 'master'; - comp.$store.state.projects.abcproject = { - web_url: '', - branches: { - master: { - workingReference: '1', - }, - }, - }; - - comp.$store.state.rightPanelCollapsed = false; - comp.$store.state.currentBranch = 'master'; - comp.$store.state.openFiles = [file('file1'), file('file2')]; - comp.$store.state.openFiles.forEach(f => Object.assign(f, { - changed: true, - content: 'testing', - })); - - return comp.$mount(); - } - - beforeEach((done) => { - vm = createComponent(); - - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - - Vue.nextTick(done); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a commit section', () => { - const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')]; - const submitCommit = vm.$el.querySelector('form .btn'); - - expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); - expect(changedFileElements.length).toEqual(2); - - changedFileElements.forEach((changedFile, i) => { - expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path); - }); - - expect(submitCommit.disabled).toBeTruthy(); - expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); - }); - - describe('when submitting', () => { - let changedFiles; - - beforeEach(() => { - vm.commitMessage = 'testing'; - changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles)); - - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - short_id: '1', - stats: {}, - }, - })); - }); - - it('allows you to submit', () => { - expect(vm.$el.querySelector('form .btn').disabled).toBeTruthy(); - }); - - it('submits commit', (done) => { - vm.makeCommit(); - - // Wait for the branch check to finish - getSetTimeoutPromise() - .then(() => Vue.nextTick()) - .then(() => { - const args = service.commit.calls.allArgs()[0]; - const { commit_message, actions, branch: payloadBranch } = args[1]; - - expect(commit_message).toBe('testing'); - expect(actions.length).toEqual(2); - expect(payloadBranch).toEqual('master'); - expect(actions[0].action).toEqual('update'); - expect(actions[1].action).toEqual('update'); - expect(actions[0].content).toEqual(changedFiles[0].content); - expect(actions[1].content).toEqual(changedFiles[1].content); - expect(actions[0].file_path).toEqual(changedFiles[0].path); - expect(actions[1].file_path).toEqual(changedFiles[1].path); - }) - .then(done) - .catch(done.fail); - }); - - it('redirects to MR creation page if start new MR checkbox checked', (done) => { - spyOn(urlUtils, 'visitUrl'); - vm.startNewMR = true; - - vm.makeCommit(); - - getSetTimeoutPromise() - .then(() => Vue.nextTick()) - .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js deleted file mode 100644 index 2895b794506..00000000000 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoEditButton from '~/ide/components/repo_edit_button.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoEditButton', () => { - let vm; - - beforeEach(() => { - const f = file(); - const RepoEditButton = Vue.extend(repoEditButton); - - vm = new RepoEditButton({ - store, - }); - - f.active = true; - vm.$store.dispatch('setInitialData', { - canCommit: true, - onTopOfBranch: true, - }); - vm.$store.state.openFiles.push(f); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders an edit button', () => { - vm.$mount(); - - expect(vm.$el.querySelector('.btn')).not.toBeNull(); - expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit'); - }); - - it('renders edit button with cancel text', () => { - vm.$store.state.editMode = true; - - vm.$mount(); - - expect(vm.$el.querySelector('.btn')).not.toBeNull(); - expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit'); - }); - - it('toggles edit mode on click', (done) => { - vm.$mount(); - - vm.$el.querySelector('.btn').click(); - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit'); - - done(); - }); - }); - - describe('discardPopupOpen', () => { - beforeEach(() => { - vm.$store.state.discardPopupOpen = true; - vm.$store.state.editMode = true; - vm.$store.state.openFiles[0].changed = true; - - vm.$mount(); - }); - - it('renders popup', () => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - }); - - it('removes all changed files', (done) => { - vm.$el.querySelector('.btn-warning').click(); - - vm.$nextTick(() => { - expect(vm.$store.getters.changedFiles.length).toBe(0); - expect(vm.$el.querySelector('.modal')).toBeNull(); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js deleted file mode 100644 index e7b2ed08acd..00000000000 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoEditor from '~/ide/components/repo_editor.vue'; -import monacoLoader from '~/ide/monaco_loader'; -import { file, resetStore } from '../helpers'; - -describe('RepoEditor', () => { - let vm; - - beforeEach((done) => { - const f = file(); - const RepoEditor = Vue.extend(repoEditor); - - vm = new RepoEditor({ - store, - }); - - f.active = true; - f.tempFile = true; - vm.$store.state.openFiles.push(f); - vm.$store.getters.activeFile.html = 'testing'; - vm.monaco = true; - - vm.$mount(); - - monacoLoader(['vs/editor/editor.main'], () => { - setTimeout(done, 0); - }); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders an ide container', (done) => { - Vue.nextTick(() => { - expect(vm.shouldHideEditor).toBeFalsy(); - - done(); - }); - }); - - describe('when open file is binary and not raw', () => { - beforeEach((done) => { - vm.$store.getters.activeFile.binary = true; - - Vue.nextTick(done); - }); - - it('does not render the IDE', () => { - expect(vm.shouldHideEditor).toBeTruthy(); - }); - - it('shows activeFile html', () => { - expect(vm.$el.textContent).toContain('testing'); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js deleted file mode 100644 index 115569a9117..00000000000 --- a/spec/javascripts/repo/components/repo_file_buttons_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoFileButtons from '~/ide/components/repo_file_buttons.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoFileButtons', () => { - const activeFile = file(); - let vm; - - function createComponent() { - const RepoFileButtons = Vue.extend(repoFileButtons); - - activeFile.rawPath = 'test'; - activeFile.blamePath = 'test'; - activeFile.commitsPath = 'test'; - activeFile.active = true; - store.state.openFiles.push(activeFile); - - return new RepoFileButtons({ - store, - }).$mount(); - } - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => { - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - const blame = vm.$el.querySelector('.blame'); - const history = vm.$el.querySelector('.history'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.textContent.trim()).toEqual('Raw'); - expect(blame.href).toMatch(`/${activeFile.blamePath}`); - expect(blame.textContent.trim()).toEqual('Blame'); - expect(history.href).toMatch(`/${activeFile.commitsPath}`); - expect(history.textContent.trim()).toEqual('History'); - expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink'); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js deleted file mode 100644 index 27b55ed1f87..00000000000 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoFile from '~/ide/components/repo_file.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoFile', () => { - const updated = 'updated'; - let vm; - - function createComponent(propsData) { - const RepoFile = Vue.extend(repoFile); - - return new RepoFile({ - store, - propsData, - }).$mount(); - } - - afterEach(() => { - resetStore(vm.$store); - }); - - it('renders link, icon and name', () => { - const RepoFile = Vue.extend(repoFile); - vm = new RepoFile({ - store, - propsData: { - file: file('t4'), - }, - }); - spyOn(vm, 'timeFormated').and.returnValue(updated); - vm.$mount(); - - const name = vm.$el.querySelector('.repo-file-name'); - - expect(name.href).toMatch(''); - expect(name.textContent.trim()).toEqual(vm.file.name); - }); - - it('does render if hasFiles is true and is loading tree', () => { - vm = createComponent({ - file: file('t1'), - }); - - expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); - }); - - it('does not render commit message and datetime if mini', (done) => { - vm = createComponent({ - file: file('t2'), - }); - vm.$store.state.openFiles.push(vm.file); - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.commit-message')).toBeFalsy(); - expect(vm.$el.querySelector('.commit-update')).toBeFalsy(); - - done(); - }); - }); - - it('fires clickFile when the link is clicked', () => { - vm = createComponent({ - file: file('t3'), - }); - - spyOn(vm, 'clickFile'); - - vm.$el.click(); - - expect(vm.clickFile).toHaveBeenCalledWith(vm.file); - }); - - describe('submodule', () => { - let f; - - beforeEach(() => { - f = file('submodule name', '123456789'); - f.type = 'submodule'; - - vm = createComponent({ - file: f, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders submodule short ID', () => { - expect(vm.$el.querySelector('.commit-sha').textContent.trim()).toBe('12345678'); - }); - - it('renders ID next to submodule name', () => { - expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678'); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js deleted file mode 100644 index 18366fb89bc..00000000000 --- a/spec/javascripts/repo/components/repo_loading_file_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoLoadingFile from '~/ide/components/repo_loading_file.vue'; -import { resetStore } from '../helpers'; - -describe('RepoLoadingFile', () => { - let vm; - - function createComponent() { - const RepoLoadingFile = Vue.extend(repoLoadingFile); - - return new RepoLoadingFile({ - store, - }).$mount(); - } - - function assertLines(lines) { - lines.forEach((line, n) => { - const index = n + 1; - expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy(); - }); - } - - function assertColumns(columns) { - columns.forEach((column) => { - const container = column.querySelector('.animation-container'); - const lines = [...container.querySelectorAll(':scope > div')]; - - expect(container).toBeTruthy(); - expect(lines.length).toEqual(6); - assertLines(lines); - }); - } - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders 3 columns of animated LoC', () => { - vm = createComponent(); - const columns = [...vm.$el.querySelectorAll('td')]; - - expect(columns.length).toEqual(3); - assertColumns(columns); - }); - - it('renders 1 column of animated LoC if isMini', (done) => { - vm = createComponent(); - vm.$store.state.leftPanelCollapsed = true; - vm.$store.state.openFiles.push('test'); - - vm.$nextTick(() => { - const columns = [...vm.$el.querySelectorAll('td')]; - - expect(columns.length).toEqual(1); - assertColumns(columns); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js deleted file mode 100644 index ff26cab2262..00000000000 --- a/spec/javascripts/repo/components/repo_prev_directory_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoPrevDirectory from '~/ide/components/repo_prev_directory.vue'; -import { resetStore } from '../helpers'; - -describe('RepoPrevDirectory', () => { - let vm; - const parentLink = 'parent'; - function createComponent() { - const RepoPrevDirectory = Vue.extend(repoPrevDirectory); - - const comp = new RepoPrevDirectory({ - store, - }); - - comp.$store.state.parentTreeUrl = parentLink; - - return comp.$mount(); - } - - beforeEach(() => { - vm = createComponent(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a prev dir link', () => { - const link = vm.$el.querySelector('a'); - - expect(link.href).toMatch(`/${parentLink}`); - expect(link.textContent).toEqual('...'); - }); - - it('clicking row triggers getTreeData', () => { - spyOn(vm, 'getTreeData'); - - vm.$el.querySelector('td').click(); - - expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js deleted file mode 100644 index e90837e4cb2..00000000000 --- a/spec/javascripts/repo/components/repo_preview_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoPreview from '~/ide/components/repo_preview.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoPreview', () => { - let vm; - - function createComponent() { - const f = file(); - const RepoPreview = Vue.extend(repoPreview); - - const comp = new RepoPreview({ - store, - }); - - f.active = true; - f.html = 'test'; - - comp.$store.state.openFiles.push(f); - - return comp.$mount(); - } - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a div with the activeFile html', () => { - vm = createComponent(); - - expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.innerHTML).toContain('test'); - }); -}); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js deleted file mode 100644 index 933e8d3a06a..00000000000 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoTab from '~/ide/components/repo_tab.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoTab', () => { - let vm; - - function createComponent(propsData) { - const RepoTab = Vue.extend(repoTab); - - return new RepoTab({ - store, - propsData, - }).$mount(); - } - - afterEach(() => { - resetStore(vm.$store); - }); - - it('renders a close link and a name link', () => { - vm = createComponent({ - tab: file(), - }); - vm.$store.state.openFiles.push(vm.tab); - const close = vm.$el.querySelector('.multi-file-tab-close'); - const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`); - - expect(close.querySelector('.fa-times')).toBeTruthy(); - expect(name.textContent.trim()).toEqual(vm.tab.name); - }); - - it('fires clickFile when the link is clicked', () => { - vm = createComponent({ - tab: file(), - }); - - spyOn(vm, 'clickFile'); - - vm.$el.click(); - - expect(vm.clickFile).toHaveBeenCalledWith(vm.tab); - }); - - it('calls closeFile when clicking close button', () => { - vm = createComponent({ - tab: file(), - }); - - spyOn(vm, 'closeFile'); - - vm.$el.querySelector('.multi-file-tab-close').click(); - - expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab }); - }); - - it('renders an fa-circle icon if tab is changed', () => { - const tab = file('changedFile'); - tab.changed = true; - vm = createComponent({ - tab, - }); - - expect(vm.$el.querySelector('.multi-file-tab-close .fa-circle')).not.toBeNull(); - }); - - describe('methods', () => { - describe('closeTab', () => { - it('does not close tab if is changed', (done) => { - const tab = file('closeFile'); - tab.changed = true; - tab.opened = true; - vm = createComponent({ - tab, - }); - vm.$store.state.openFiles.push(tab); - vm.$store.dispatch('setFileActive', tab); - - vm.$el.querySelector('.multi-file-tab-close').click(); - - vm.$nextTick(() => { - expect(tab.opened).toBeTruthy(); - - done(); - }); - }); - - it('closes tab when clicking close btn', (done) => { - const tab = file('lose'); - tab.opened = true; - vm = createComponent({ - tab, - }); - vm.$store.state.openFiles.push(tab); - vm.$store.dispatch('setFileActive', tab); - - vm.$el.querySelector('.multi-file-tab-close').click(); - - vm.$nextTick(() => { - expect(tab.opened).toBeFalsy(); - - done(); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js deleted file mode 100644 index 2c363364d70..00000000000 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoTabs from '~/ide/components/repo_tabs.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoTabs', () => { - const openedFiles = [file('open1'), file('open2')]; - let vm; - - function createComponent() { - const RepoTabs = Vue.extend(repoTabs); - - return new RepoTabs({ - store, - }).$mount(); - } - - afterEach(() => { - resetStore(vm.$store); - }); - - it('renders a list of tabs', (done) => { - vm = createComponent(); - openedFiles[0].active = true; - vm.$store.state.openFiles = openedFiles; - - vm.$nextTick(() => { - const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')]; - - expect(tabs.length).toEqual(2); - expect(tabs[0].classList.contains('active')).toBeTruthy(); - expect(tabs[1].classList.contains('active')).toBeFalsy(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js deleted file mode 100644 index ac43d221198..00000000000 --- a/spec/javascripts/repo/helpers.js +++ /dev/null @@ -1,16 +0,0 @@ -import { decorateData } from '~/ide/stores/utils'; -import state from '~/ide/stores/state'; - -export const resetStore = (store) => { - store.replaceState(state()); -}; - -export const file = (name = 'name', id = name, type = '') => decorateData({ - id, - type, - icon: 'icon', - url: 'url', - name, - path: name, - lastCommit: {}, -}); diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js deleted file mode 100644 index af12ca15369..00000000000 --- a/spec/javascripts/repo/lib/common/disposable_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import Disposable from '~/ide/lib/common/disposable'; - -describe('Multi-file editor library disposable class', () => { - let instance; - let disposableClass; - - beforeEach(() => { - instance = new Disposable(); - - disposableClass = { - dispose: jasmine.createSpy('dispose'), - }; - }); - - afterEach(() => { - instance.dispose(); - }); - - describe('add', () => { - it('adds disposable classes', () => { - instance.add(disposableClass); - - expect(instance.disposers.size).toBe(1); - }); - }); - - describe('dispose', () => { - beforeEach(() => { - instance.add(disposableClass); - }); - - it('calls dispose on all cached disposers', () => { - instance.dispose(); - - expect(disposableClass.dispose).toHaveBeenCalled(); - }); - - it('clears cached disposers', () => { - instance.dispose(); - - expect(instance.disposers.size).toBe(0); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js deleted file mode 100644 index 563c2e33834..00000000000 --- a/spec/javascripts/repo/lib/common/model_manager_spec.js +++ /dev/null @@ -1,81 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import ModelManager from '~/ide/lib/common/model_manager'; -import { file } from '../../helpers'; - -describe('Multi-file editor library model manager', () => { - let instance; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - instance = new ModelManager(monaco); - - done(); - }); - }); - - afterEach(() => { - instance.dispose(); - }); - - describe('addModel', () => { - it('caches model', () => { - instance.addModel(file()); - - expect(instance.models.size).toBe(1); - }); - - it('caches model by file path', () => { - instance.addModel(file('path-name')); - - expect(instance.models.keys().next().value).toBe('path-name'); - }); - - it('adds model into disposable', () => { - spyOn(instance.disposable, 'add').and.callThrough(); - - instance.addModel(file()); - - expect(instance.disposable.add).toHaveBeenCalled(); - }); - - it('returns cached model', () => { - spyOn(instance.models, 'get').and.callThrough(); - - instance.addModel(file()); - instance.addModel(file()); - - expect(instance.models.get).toHaveBeenCalled(); - }); - }); - - describe('hasCachedModel', () => { - it('returns false when no models exist', () => { - expect(instance.hasCachedModel('path')).toBeFalsy(); - }); - - it('returns true when model exists', () => { - instance.addModel(file('path-name')); - - expect(instance.hasCachedModel('path-name')).toBeTruthy(); - }); - }); - - describe('dispose', () => { - it('clears cached models', () => { - instance.addModel(file()); - - instance.dispose(); - - expect(instance.models.size).toBe(0); - }); - - it('calls disposable dispose', () => { - spyOn(instance.disposable, 'dispose').and.callThrough(); - - instance.dispose(); - - expect(instance.disposable.dispose).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js deleted file mode 100644 index 878a4a3f3fe..00000000000 --- a/spec/javascripts/repo/lib/common/model_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import Model from '~/ide/lib/common/model'; -import { file } from '../../helpers'; - -describe('Multi-file editor library model', () => { - let model; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - model = new Model(monaco, file('path')); - - done(); - }); - }); - - afterEach(() => { - model.dispose(); - }); - - it('creates original model & new model', () => { - expect(model.originalModel).not.toBeNull(); - expect(model.model).not.toBeNull(); - }); - - describe('path', () => { - it('returns file path', () => { - expect(model.path).toBe('path'); - }); - }); - - describe('getModel', () => { - it('returns model', () => { - expect(model.getModel()).toBe(model.model); - }); - }); - - describe('getOriginalModel', () => { - it('returns original model', () => { - expect(model.getOriginalModel()).toBe(model.originalModel); - }); - }); - - describe('onChange', () => { - it('caches event by path', () => { - model.onChange(() => {}); - - expect(model.events.size).toBe(1); - expect(model.events.keys().next().value).toBe('path'); - }); - - it('calls callback on change', (done) => { - const spy = jasmine.createSpy(); - model.onChange(spy); - - model.getModel().setValue('123'); - - setTimeout(() => { - expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything()); - done(); - }); - }); - }); - - describe('dispose', () => { - it('calls disposable dispose', () => { - spyOn(model.disposable, 'dispose').and.callThrough(); - - model.dispose(); - - expect(model.disposable.dispose).toHaveBeenCalled(); - }); - - it('clears events', () => { - model.onChange(() => {}); - - expect(model.events.size).toBe(1); - - model.dispose(); - - expect(model.events.size).toBe(0); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js deleted file mode 100644 index fea12d74dca..00000000000 --- a/spec/javascripts/repo/lib/decorations/controller_spec.js +++ /dev/null @@ -1,120 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; -import DecorationsController from '~/ide/lib/decorations/controller'; -import Model from '~/ide/lib/common/model'; -import { file } from '../../helpers'; - -describe('Multi-file editor library decorations controller', () => { - let editorInstance; - let controller; - let model; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - editorInstance = editor.create(monaco); - editorInstance.createInstance(document.createElement('div')); - - controller = new DecorationsController(editorInstance); - model = new Model(monaco, file('path')); - - done(); - }); - }); - - afterEach(() => { - model.dispose(); - editorInstance.dispose(); - controller.dispose(); - }); - - describe('getAllDecorationsForModel', () => { - it('returns empty array when no decorations exist for model', () => { - const decorations = controller.getAllDecorationsForModel(model); - - expect(decorations).toEqual([]); - }); - - it('returns decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - const decorations = controller.getAllDecorationsForModel(model); - - expect(decorations[0]).toEqual({ decoration: 'decorationValue' }); - }); - }); - - describe('addDecorations', () => { - it('caches decorations in a new map', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorations.size).toBe(1); - }); - - it('does not create new cache model', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]); - - expect(controller.decorations.size).toBe(1); - }); - - it('caches decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorations.size).toBe(1); - expect(controller.decorations.keys().next().value).toBe('path'); - }); - - it('calls decorate method', () => { - spyOn(controller, 'decorate'); - - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorate).toHaveBeenCalled(); - }); - }); - - describe('decorate', () => { - it('sets decorations on editor instance', () => { - spyOn(controller.editor.instance, 'deltaDecorations'); - - controller.decorate(model); - - expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []); - }); - - it('caches decorations', () => { - spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); - - controller.decorate(model); - - expect(controller.editorDecorations.size).toBe(1); - }); - - it('caches decorations by model URL', () => { - spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); - - controller.decorate(model); - - expect(controller.editorDecorations.keys().next().value).toBe('path'); - }); - }); - - describe('dispose', () => { - it('clears cached decorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - controller.dispose(); - - expect(controller.decorations.size).toBe(0); - }); - - it('clears cached editorDecorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - controller.dispose(); - - expect(controller.editorDecorations.size).toBe(0); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js deleted file mode 100644 index 1d55c165260..00000000000 --- a/spec/javascripts/repo/lib/diff/controller_spec.js +++ /dev/null @@ -1,176 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; -import ModelManager from '~/ide/lib/common/model_manager'; -import DecorationsController from '~/ide/lib/decorations/controller'; -import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; -import { computeDiff } from '~/ide/lib/diff/diff'; -import { file } from '../../helpers'; - -describe('Multi-file editor library dirty diff controller', () => { - let editorInstance; - let controller; - let modelManager; - let decorationsController; - let model; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - editorInstance = editor.create(monaco); - editorInstance.createInstance(document.createElement('div')); - - modelManager = new ModelManager(monaco); - decorationsController = new DecorationsController(editorInstance); - - model = modelManager.addModel(file()); - - controller = new DirtyDiffController(modelManager, decorationsController); - - done(); - }); - }); - - afterEach(() => { - controller.dispose(); - model.dispose(); - decorationsController.dispose(); - editorInstance.dispose(); - }); - - describe('getDiffChangeType', () => { - ['added', 'removed', 'modified'].forEach((type) => { - it(`returns ${type}`, () => { - const change = { - [type]: true, - }; - - expect(getDiffChangeType(change)).toBe(type); - }); - }); - }); - - describe('getDecorator', () => { - ['added', 'removed', 'modified'].forEach((type) => { - it(`returns with linesDecorationsClassName for ${type}`, () => { - const change = { - [type]: true, - }; - - expect( - getDecorator(change).options.linesDecorationsClassName, - ).toBe(`dirty-diff dirty-diff-${type}`); - }); - - it('returns with line numbers', () => { - const change = { - lineNumber: 1, - endLineNumber: 2, - [type]: true, - }; - - const range = getDecorator(change).range; - - expect(range.startLineNumber).toBe(1); - expect(range.endLineNumber).toBe(2); - expect(range.startColumn).toBe(1); - expect(range.endColumn).toBe(1); - }); - }); - }); - - describe('attachModel', () => { - it('adds change event callback', () => { - spyOn(model, 'onChange'); - - controller.attachModel(model); - - expect(model.onChange).toHaveBeenCalled(); - }); - - it('calls throttledComputeDiff on change', () => { - spyOn(controller, 'throttledComputeDiff'); - - controller.attachModel(model); - - model.getModel().setValue('123'); - - expect(controller.throttledComputeDiff).toHaveBeenCalled(); - }); - }); - - describe('computeDiff', () => { - it('posts to worker', () => { - spyOn(controller.dirtyDiffWorker, 'postMessage'); - - controller.computeDiff(model); - - expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({ - path: model.path, - originalContent: '', - newContent: '', - }); - }); - }); - - describe('reDecorate', () => { - it('calls decorations controller decorate', () => { - spyOn(controller.decorationsController, 'decorate'); - - controller.reDecorate(model); - - expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); - }); - }); - - describe('decorate', () => { - it('adds decorations into decorations controller', () => { - spyOn(controller.decorationsController, 'addDecorations'); - - controller.decorate({ data: { changes: [], path: 'path' } }); - - expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything()); - }); - - it('adds decorations into editor', () => { - const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); - - controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } }); - - expect(spy).toHaveBeenCalledWith([], [{ - range: new monaco.Range( - 1, 1, 1, 1, - ), - options: { - isWholeLine: true, - linesDecorationsClassName: 'dirty-diff dirty-diff-modified', - }, - }]); - }); - }); - - describe('dispose', () => { - it('calls disposable dispose', () => { - spyOn(controller.disposable, 'dispose').and.callThrough(); - - controller.dispose(); - - expect(controller.disposable.dispose).toHaveBeenCalled(); - }); - - it('terminates worker', () => { - spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough(); - - controller.dispose(); - - expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled(); - }); - - it('removes worker event listener', () => { - spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); - - controller.dispose(); - - expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything()); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js deleted file mode 100644 index 57f3ac3d365..00000000000 --- a/spec/javascripts/repo/lib/diff/diff_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import { computeDiff } from '~/ide/lib/diff/diff'; - -describe('Multi-file editor library diff calculator', () => { - describe('computeDiff', () => { - it('returns empty array if no changes', () => { - const diff = computeDiff('123', '123'); - - expect(diff).toEqual([]); - }); - - describe('modified', () => { - it('', () => { - const diff = computeDiff('123', '1234')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(2); - }); - }); - - describe('added', () => { - it('', () => { - const diff = computeDiff('123', '123\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(3); - }); - }); - - describe('removed', () => { - it('', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeTruthy(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeTruthy(); - expect(diff.lineNumber).toBe(2); - }); - }); - - it('includes line number of change', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.lineNumber).toBe(1); - }); - - it('includes end line number of change', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.endLineNumber).toBe(1); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js deleted file mode 100644 index edbf5450dce..00000000000 --- a/spec/javascripts/repo/lib/editor_options_spec.js +++ /dev/null @@ -1,7 +0,0 @@ -import editorOptions from '~/ide/lib/editor_options'; - -describe('Multi-file editor library editor options', () => { - it('returns an array', () => { - expect(editorOptions).toEqual(jasmine.any(Array)); - }); -}); diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js deleted file mode 100644 index 8d51d48a782..00000000000 --- a/spec/javascripts/repo/lib/editor_spec.js +++ /dev/null @@ -1,128 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; -import { file } from '../helpers'; - -describe('Multi-file editor library', () => { - let instance; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - instance = editor.create(monaco); - - done(); - }); - }); - - afterEach(() => { - instance.dispose(); - }); - - it('creates instance of editor', () => { - expect(editor.editorInstance).not.toBeNull(); - }); - - describe('createInstance', () => { - let el; - - beforeEach(() => { - el = document.createElement('div'); - }); - - it('creates editor instance', () => { - spyOn(instance.monaco.editor, 'create').and.callThrough(); - - instance.createInstance(el); - - expect(instance.monaco.editor.create).toHaveBeenCalled(); - }); - - it('creates dirty diff controller', () => { - instance.createInstance(el); - - expect(instance.dirtyDiffController).not.toBeNull(); - }); - }); - - describe('createModel', () => { - it('calls model manager addModel', () => { - spyOn(instance.modelManager, 'addModel'); - - instance.createModel('FILE'); - - expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE'); - }); - }); - - describe('attachModel', () => { - let model; - - beforeEach(() => { - instance.createInstance(document.createElement('div')); - - model = instance.createModel(file()); - }); - - it('sets the current model on the instance', () => { - instance.attachModel(model); - - expect(instance.currentModel).toBe(model); - }); - - it('attaches the model to the current instance', () => { - spyOn(instance.instance, 'setModel'); - - instance.attachModel(model); - - expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); - }); - - it('attaches the model to the dirty diff controller', () => { - spyOn(instance.dirtyDiffController, 'attachModel'); - - instance.attachModel(model); - - expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model); - }); - - it('re-decorates with the dirty diff controller', () => { - spyOn(instance.dirtyDiffController, 'reDecorate'); - - instance.attachModel(model); - - expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model); - }); - }); - - describe('clearEditor', () => { - it('resets the editor model', () => { - instance.createInstance(document.createElement('div')); - - spyOn(instance.instance, 'setModel'); - - instance.clearEditor(); - - expect(instance.instance.setModel).toHaveBeenCalledWith(null); - }); - }); - - describe('dispose', () => { - it('calls disposble dispose method', () => { - spyOn(instance.disposable, 'dispose').and.callThrough(); - - instance.dispose(); - - expect(instance.disposable.dispose).toHaveBeenCalled(); - }); - - it('resets instance', () => { - instance.createInstance(document.createElement('div')); - - expect(instance.instance).not.toBeNull(); - - instance.dispose(); - - expect(instance.instance).toBeNull(); - }); - }); -}); diff --git a/spec/javascripts/repo/monaco_loader_spec.js b/spec/javascripts/repo/monaco_loader_spec.js deleted file mode 100644 index b8ac36972aa..00000000000 --- a/spec/javascripts/repo/monaco_loader_spec.js +++ /dev/null @@ -1,13 +0,0 @@ -import monacoContext from 'monaco-editor/dev/vs/loader'; -import monacoLoader from '~/ide/monaco_loader'; - -describe('MonacoLoader', () => { - it('calls require.config and exports require', () => { - expect(monacoContext.require.getConfig()).toEqual(jasmine.objectContaining({ - paths: { - vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase - }, - })); - expect(monacoLoader).toBe(monacoContext.require); - }); -}); diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js deleted file mode 100644 index 00d16fd790d..00000000000 --- a/spec/javascripts/repo/stores/actions/branch_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { resetStore } from '../../helpers'; - -describe('Multi-file store branch actions', () => { - afterEach(() => { - resetStore(store); - }); - - describe('createNewBranch', () => { - beforeEach(() => { - spyOn(service, 'createBranch').and.returnValue(Promise.resolve({ - json: () => ({ - name: 'testing', - }), - })); - spyOn(history, 'pushState'); - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'testing'; - store.state.projects.abcproject = { - branches: { - master: { - workingReference: '1', - }, - }, - }; - }); - - it('creates new branch', (done) => { - store.dispatch('createNewBranch', 'master') - .then(() => { - expect(store.state.currentBranchId).toBe('testing'); - expect(service.createBranch).toHaveBeenCalledWith('abcproject', { - branch: 'master', - ref: 'testing', - }); - - done(); - }) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js deleted file mode 100644 index e2d8f002e27..00000000000 --- a/spec/javascripts/repo/stores/actions/file_spec.js +++ /dev/null @@ -1,431 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { file, resetStore } from '../../helpers'; - -describe('Multi-file store file actions', () => { - afterEach(() => { - resetStore(store); - }); - - describe('closeFile', () => { - let localFile; - let getLastCommitDataSpy; - let oldGetLastCommitData; - - beforeEach(() => { - getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); - oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line - store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line - - localFile = file('testFile'); - localFile.active = true; - localFile.opened = true; - localFile.parentTreeUrl = 'parentTreeUrl'; - - store.state.openFiles.push(localFile); - }); - - afterEach(() => { - store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line - }); - - it('closes open files', (done) => { - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(localFile.opened).toBeFalsy(); - expect(localFile.active).toBeFalsy(); - expect(store.state.openFiles.length).toBe(0); - - done(); - }).catch(done.fail); - }); - - it('does not close file if has changed', (done) => { - localFile.changed = true; - - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(localFile.opened).toBeTruthy(); - expect(localFile.active).toBeTruthy(); - expect(store.state.openFiles.length).toBe(1); - - done(); - }).catch(done.fail); - }); - - it('does not close file if temp file', (done) => { - localFile.tempFile = true; - - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(localFile.opened).toBeTruthy(); - expect(localFile.active).toBeTruthy(); - expect(store.state.openFiles.length).toBe(1); - - done(); - }).catch(done.fail); - }); - - it('force closes a changed file', (done) => { - localFile.changed = true; - - store.dispatch('closeFile', { file: localFile, force: true }) - .then(() => { - expect(localFile.opened).toBeFalsy(); - expect(localFile.active).toBeFalsy(); - expect(store.state.openFiles.length).toBe(0); - - done(); - }).catch(done.fail); - }); - - it('sets next file as active', (done) => { - const f = file('otherfile'); - store.state.openFiles.push(f); - - expect(f.active).toBeFalsy(); - - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(f.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('calls getLastCommitData', (done) => { - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(getLastCommitDataSpy).toHaveBeenCalled(); - - done(); - }).catch(done.fail); - }); - }); - - describe('setFileActive', () => { - let scrollToTabSpy; - let oldScrollToTab; - - beforeEach(() => { - scrollToTabSpy = jasmine.createSpy('scrollToTab'); - oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line - store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line - }); - - afterEach(() => { - store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line - }); - - it('calls scrollToTab', (done) => { - store.dispatch('setFileActive', file('setThisActive')) - .then(() => { - expect(scrollToTabSpy).toHaveBeenCalled(); - - done(); - }).catch(done.fail); - }); - - it('sets the file active', (done) => { - const localFile = file('activeFile'); - - store.dispatch('setFileActive', localFile) - .then(() => { - expect(localFile.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('returns early if file is already active', (done) => { - const localFile = file('earlyActive'); - localFile.active = true; - - store.dispatch('setFileActive', localFile) - .then(() => { - expect(scrollToTabSpy).not.toHaveBeenCalled(); - - done(); - }).catch(done.fail); - }); - - it('sets current active file to not active', (done) => { - const localFile = file('currentActive'); - localFile.active = true; - store.state.openFiles.push(localFile); - - store.dispatch('setFileActive', file('newActive')) - .then(() => { - expect(localFile.active).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - - it('resets location.hash for line highlighting', (done) => { - location.hash = 'test'; - - store.dispatch('setFileActive', file('otherActive')) - .then(() => { - expect(location.hash).not.toBe('test'); - - done(); - }).catch(done.fail); - }); - }); - - describe('getFileData', () => { - let localFile; - - beforeEach(() => { - spyOn(service, 'getFileData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'testing getFileData', - }, - json: () => Promise.resolve({ - blame_path: 'blame_path', - commits_path: 'commits_path', - permalink: 'permalink', - raw_path: 'raw_path', - binary: false, - html: '123', - render_error: '', - }), - })); - - localFile = file('newCreate'); - localFile.url = 'getFileDataURL'; - }); - - afterEach(() => { - store.dispatch('closeFile', { - file: localFile, - force: true, - }); - }); - - it('calls the service', (done) => { - store.dispatch('getFileData', localFile) - .then(() => { - expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); - - done(); - }).catch(done.fail); - }); - - it('sets the file data', (done) => { - store.dispatch('getFileData', localFile) - .then(Vue.nextTick) - .then(() => { - expect(localFile.blamePath).toBe('blame_path'); - - done(); - }).catch(done.fail); - }); - - it('sets document title', (done) => { - store.dispatch('getFileData', localFile) - .then(() => { - expect(document.title).toBe('testing getFileData'); - - done(); - }).catch(done.fail); - }); - - it('sets the file as active', (done) => { - store.dispatch('getFileData', localFile) - .then(Vue.nextTick) - .then(() => { - expect(localFile.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('adds the file to open files', (done) => { - store.dispatch('getFileData', localFile) - .then(Vue.nextTick) - .then(() => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(localFile.name); - - done(); - }).catch(done.fail); - }); - - it('toggles the file loading', (done) => { - store.dispatch('getFileData', localFile) - .then(() => { - expect(localFile.loading).toBeTruthy(); - - return Vue.nextTick(); - }) - .then(() => { - expect(localFile.loading).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - }); - - describe('getRawFileData', () => { - let tmpFile; - - beforeEach(() => { - spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); - - tmpFile = file('tmpFile'); - }); - - it('calls getRawFileData service method', (done) => { - store.dispatch('getRawFileData', tmpFile) - .then(() => { - expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); - - done(); - }).catch(done.fail); - }); - - it('updates file raw data', (done) => { - store.dispatch('getRawFileData', tmpFile) - .then(() => { - expect(tmpFile.raw).toBe('raw'); - - done(); - }).catch(done.fail); - }); - }); - - describe('changeFileContent', () => { - let tmpFile; - - beforeEach(() => { - tmpFile = file('tmpFile'); - }); - - it('updates file content', (done) => { - store.dispatch('changeFileContent', { - file: tmpFile, - content: 'content', - }) - .then(() => { - expect(tmpFile.content).toBe('content'); - - done(); - }).catch(done.fail); - }); - }); - - describe('createTempFile', () => { - let projectTree; - - beforeEach(() => { - document.body.innerHTML += '<div class="flash-container"></div>'; - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - branches: { - master: { - workingReference: '1', - }, - }, - }; - - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - - projectTree = store.state.trees['abcproject/mybranch']; - }); - - afterEach(() => { - document.querySelector('.flash-container').remove(); - }); - - it('creates temp file', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(f.tempFile).toBeTruthy(); - expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1); - - done(); - }).catch(done.fail); - }); - - it('adds tmp file to open files', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(f.name); - - done(); - }).catch(done.fail); - }); - - it('sets tmp file as active', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(f.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('enters edit mode if file is not base64', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then(() => { - expect(store.state.editMode).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('creates flash message is file already exists', (done) => { - store.state.trees['abcproject/mybranch'].tree.push(file('test', '1', 'blob')); - - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then(() => { - expect(document.querySelector('.flash-alert')).not.toBeNull(); - - done(); - }).catch(done.fail); - }); - - it('increases level of file', (done) => { - store.state.trees['abcproject/mybranch'].level = 1; - - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(f.level).toBe(2); - - done(); - }).catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js deleted file mode 100644 index 65351dbb7d9..00000000000 --- a/spec/javascripts/repo/stores/actions/tree_spec.js +++ /dev/null @@ -1,350 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { file, resetStore } from '../../helpers'; - -describe('Multi-file store tree actions', () => { - let projectTree; - - const basicCallParameters = { - endpoint: 'rootEndpoint', - projectId: 'abcproject', - branch: 'master', - }; - - beforeEach(() => { - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - web_url: '', - branches: { - master: { - workingReference: '1', - }, - }, - }; - }); - - afterEach(() => { - resetStore(store); - }); - - describe('getTreeData', () => { - beforeEach(() => { - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - }); - - it('calls service getTreeData', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint'); - - done(); - }).catch(done.fail); - }); - - it('adds data into tree', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - projectTree = store.state.trees['abcproject/master']; - expect(projectTree.tree.length).toBe(3); - expect(projectTree.tree[0].type).toBe('tree'); - expect(projectTree.tree[1].type).toBe('submodule'); - expect(projectTree.tree[2].type).toBe('blob'); - - done(); - }).catch(done.fail); - }); - - it('sets parent tree URL', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(store.state.parentTreeUrl).toBe('parent_tree_url'); - - done(); - }).catch(done.fail); - }); - - it('sets last commit path', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(store.state.trees['abcproject/master'].lastCommitPath).toBe('last_commit_path'); - - done(); - }).catch(done.fail); - }); - - it('sets root if not currently at root', (done) => { - store.state.isInitialRoot = false; - - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(store.state.isInitialRoot).toBeTruthy(); - expect(store.state.isRoot).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('sets page title', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(document.title).toBe('test'); - - done(); - }).catch(done.fail); - }); - - it('calls getLastCommitData if prevLastCommitPath is not null', (done) => { - const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); - const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line - store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line - store.state.prevLastCommitPath = 'test'; - - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(getLastCommitDataSpy).toHaveBeenCalledWith(projectTree); - - store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line - - done(); - }).catch(done.fail); - }); - }); - - describe('toggleTreeOpen', () => { - let oldGetTreeData; - let getTreeDataSpy; - let tree; - - beforeEach(() => { - getTreeDataSpy = jasmine.createSpy('getTreeData'); - - oldGetTreeData = store._actions.getTreeData; // eslint-disable-line - store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line - - tree = { - projectId: 'abcproject', - branchId: 'master', - opened: false, - tree: [], - }; - }); - - afterEach(() => { - store._actions.getTreeData = oldGetTreeData; // eslint-disable-line - }); - - it('toggles the tree open', (done) => { - store.dispatch('toggleTreeOpen', { - endpoint: 'test', - tree, - }).then(() => { - expect(tree.opened).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('calls getTreeData if tree is closed', (done) => { - store.dispatch('toggleTreeOpen', { - endpoint: 'test', - tree, - }).then(() => { - expect(getTreeDataSpy).toHaveBeenCalledWith({ - projectId: 'abcproject', - branch: 'master', - endpoint: 'test', - tree, - }); - - done(); - }).catch(done.fail); - }); - - it('resets entries tree', (done) => { - Object.assign(tree, { - opened: true, - tree: ['a'], - }); - - store.dispatch('toggleTreeOpen', { - endpoint: 'test', - tree, - }).then(() => { - expect(tree.tree.length).toBe(0); - - done(); - }).catch(done.fail); - }); - }); - - describe('createTempTree', () => { - beforeEach(() => { - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - projectTree = store.state.trees['abcproject/mybranch']; - }); - - it('creates temp tree', (done) => { - store.dispatch('createTempTree', { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - name: 'test', - parent: projectTree, - }) - .then(() => { - expect(projectTree.tree[0].name).toBe('test'); - expect(projectTree.tree[0].type).toBe('tree'); - - done(); - }).catch(done.fail); - }); - - it('creates new folder inside another tree', (done) => { - const tree = { - type: 'tree', - name: 'testing', - tree: [], - }; - - projectTree.tree.push(tree); - - store.dispatch('createTempTree', { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - name: 'testing/test', - parent: projectTree, - }) - .then(() => { - expect(projectTree.tree[0].name).toBe('testing'); - expect(projectTree.tree[0].tree[0].tempFile).toBeTruthy(); - expect(projectTree.tree[0].tree[0].name).toBe('test'); - expect(projectTree.tree[0].tree[0].type).toBe('tree'); - - done(); - }).catch(done.fail); - }); - - it('does not create new tree if already exists', (done) => { - const tree = { - type: 'tree', - name: 'testing', - endpoint: 'test', - tree: [], - }; - - projectTree.tree.push(tree); - - store.dispatch('createTempTree', { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - name: 'testing/test', - parent: projectTree, - }) - .then(() => { - expect(projectTree.tree[0].name).toBe('testing'); - expect(projectTree.tree[0].tempFile).toBeUndefined(); - - done(); - }).catch(done.fail); - }); - }); - - describe('getLastCommitData', () => { - beforeEach(() => { - spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({ - headers: { - 'more-logs-url': null, - }, - json: () => Promise.resolve([{ - type: 'tree', - file_name: 'testing', - commit: { - message: 'commit message', - authored_date: '123', - }, - }]), - })); - - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - - projectTree = store.state.trees['abcproject/mybranch']; - projectTree.tree.push(file('testing', '1', 'tree')); - projectTree.lastCommitPath = 'lastcommitpath'; - }); - - it('calls service with lastCommitPath', (done) => { - store.dispatch('getLastCommitData', projectTree) - .then(() => { - expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); - - done(); - }).catch(done.fail); - }); - - it('updates trees last commit data', (done) => { - store.dispatch('getLastCommitData', projectTree) - .then(Vue.nextTick) - .then(() => { - expect(projectTree.tree[0].lastCommit.message).toBe('commit message'); - - done(); - }).catch(done.fail); - }); - - it('does not update entry if not found', (done) => { - projectTree.tree[0].name = 'a'; - - store.dispatch('getLastCommitData', projectTree) - .then(Vue.nextTick) - .then(() => { - expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message'); - - done(); - }).catch(done.fail); - }); - }); - - describe('updateDirectoryData', () => { - it('adds data into tree', (done) => { - const tree = { - tree: [], - }; - const data = { - trees: [{ name: 'tree' }], - submodules: [{ name: 'submodule' }], - blobs: [{ name: 'blob' }], - }; - - store.dispatch('updateDirectoryData', { - data, - tree, - }).then(() => { - expect(tree.tree[0].name).toBe('tree'); - expect(tree.tree[0].type).toBe('tree'); - expect(tree.tree[1].name).toBe('submodule'); - expect(tree.tree[1].type).toBe('submodule'); - expect(tree.tree[2].name).toBe('blob'); - expect(tree.tree[2].type).toBe('blob'); - - done(); - }).catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js deleted file mode 100644 index f678967b092..00000000000 --- a/spec/javascripts/repo/stores/actions_spec.js +++ /dev/null @@ -1,432 +0,0 @@ -import Vue from 'vue'; -import * as urlUtils from '~/lib/utils/url_utility'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { resetStore, file } from '../helpers'; - -describe('Multi-file store actions', () => { - afterEach(() => { - resetStore(store); - }); - - describe('redirectToUrl', () => { - it('calls visitUrl', (done) => { - spyOn(urlUtils, 'visitUrl'); - - store.dispatch('redirectToUrl', 'test') - .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith('test'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('setInitialData', () => { - it('commits initial data', (done) => { - store.dispatch('setInitialData', { canCommit: true }) - .then(() => { - expect(store.state.canCommit).toBeTruthy(); - done(); - }) - .catch(done.fail); - }); - }); - - describe('closeDiscardPopup', () => { - it('closes the discard popup', (done) => { - store.dispatch('closeDiscardPopup', false) - .then(() => { - expect(store.state.discardPopupOpen).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('discardAllChanges', () => { - beforeEach(() => { - store.state.openFiles.push(file('discardAll')); - store.state.openFiles[0].changed = true; - }); - }); - - describe('closeAllFiles', () => { - beforeEach(() => { - store.state.openFiles.push(file('closeAll')); - store.state.openFiles[0].opened = true; - }); - - it('closes all open files', (done) => { - store.dispatch('closeAllFiles') - .then(() => { - expect(store.state.openFiles.length).toBe(0); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('toggleEditMode', () => { - it('toggles edit mode', (done) => { - store.state.editMode = true; - - store.dispatch('toggleEditMode') - .then(() => { - expect(store.state.editMode).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - - it('sets preview mode', (done) => { - store.state.currentBlobView = 'repo-editor'; - store.state.editMode = true; - - store.dispatch('toggleEditMode') - .then(Vue.nextTick) - .then(() => { - expect(store.state.currentBlobView).toBe('repo-preview'); - - done(); - }).catch(done.fail); - }); - - it('opens discard popup if there are changed files', (done) => { - store.state.editMode = true; - store.state.openFiles.push(file('discardChanges')); - store.state.openFiles[0].changed = true; - - store.dispatch('toggleEditMode') - .then(() => { - expect(store.state.discardPopupOpen).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('can force closed if there are changed files', (done) => { - store.state.editMode = true; - - store.state.openFiles.push(file('forceClose')); - store.state.openFiles[0].changed = true; - - store.dispatch('toggleEditMode', true) - .then(() => { - expect(store.state.discardPopupOpen).toBeFalsy(); - expect(store.state.editMode).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - - it('discards file changes', (done) => { - const f = file('discard'); - store.state.editMode = true; - store.state.openFiles.push(f); - f.changed = true; - - store.dispatch('toggleEditMode', true) - .then(Vue.nextTick) - .then(() => { - expect(f.changed).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - }); - - describe('toggleBlobView', () => { - it('sets edit mode view if in edit mode', (done) => { - store.dispatch('toggleBlobView') - .then(() => { - expect(store.state.currentBlobView).toBe('repo-editor'); - - done(); - }) - .catch(done.fail); - }); - - it('sets preview mode view if not in edit mode', (done) => { - store.state.editMode = false; - - store.dispatch('toggleBlobView') - .then(() => { - expect(store.state.currentBlobView).toBe('repo-preview'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('checkCommitStatus', () => { - beforeEach(() => { - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - branches: { - master: { - workingReference: '1', - }, - }, - }; - }); - - it('calls service', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '123' }, - }, - })); - - store.dispatch('checkCommitStatus') - .then(() => { - expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); - - done(); - }) - .catch(done.fail); - }); - - it('returns true if current ref does not equal returned ID', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '123' }, - }, - })); - - store.dispatch('checkCommitStatus') - .then((val) => { - expect(val).toBeTruthy(); - - done(); - }) - .catch(done.fail); - }); - - it('returns false if current ref equals returned ID', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '1' }, - }, - })); - - store.dispatch('checkCommitStatus') - .then((val) => { - expect(val).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('commitChanges', () => { - let payload; - - beforeEach(() => { - spyOn(window, 'scrollTo'); - - document.body.innerHTML += '<div class="flash-container"></div>'; - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - web_url: 'webUrl', - branches: { - master: { - workingReference: '1', - }, - }, - }; - - payload = { - branch: 'master', - }; - }); - - afterEach(() => { - document.querySelector('.flash-container').remove(); - }); - - describe('success', () => { - beforeEach(() => { - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - id: '123456', - short_id: '123', - message: 'test message', - committed_date: 'date', - stats: { - additions: '1', - deletions: '2', - }, - }, - })); - }); - - it('calls service', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - expect(service.commit).toHaveBeenCalledWith('abcproject', payload); - - done(); - }).catch(done.fail); - }); - - it('shows flash notice', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - const alert = document.querySelector('.flash-container'); - - expect(alert.querySelector('.flash-notice')).not.toBeNull(); - expect(alert.textContent.trim()).toBe( - 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', - ); - - done(); - }).catch(done.fail); - }); - - it('adds commit data to changed files', (done) => { - const changedFile = file('changed'); - const f = file('newfile'); - changedFile.changed = true; - - store.state.openFiles.push(changedFile, f); - - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - expect(changedFile.lastCommit.message).toBe('test message'); - expect(f.lastCommit.message).not.toBe('test message'); - - done(); - }).catch(done.fail); - }); - - it('scrolls to top of page', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - expect(window.scrollTo).toHaveBeenCalledWith(0, 0); - - done(); - }).catch(done.fail); - }); - - it('redirects to new merge request page', (done) => { - spyOn(urlUtils, 'visitUrl'); - - store.dispatch('commitChanges', { payload, newMr: true }) - .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith('webUrl/merge_requests/new?merge_request%5Bsource_branch%5D=master'); - - done(); - }).catch(done.fail); - }); - }); - - describe('failed', () => { - beforeEach(() => { - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - message: 'failed message', - }, - })); - }); - - it('shows failed message', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - const alert = document.querySelector('.flash-container'); - - expect(alert.textContent.trim()).toBe( - 'failed message', - ); - - done(); - }).catch(done.fail); - }); - }); - }); - - describe('createTempEntry', () => { - beforeEach(() => { - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - store.state.projects.abcproject = { - web_url: '', - }; - }); - - it('creates a temp tree', (done) => { - const projectTree = store.state.trees['abcproject/mybranch']; - - store.dispatch('createTempEntry', { - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - name: 'test', - type: 'tree', - }) - .then(() => { - const baseTree = projectTree.tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].tempFile).toBeTruthy(); - expect(baseTree[0].type).toBe('tree'); - - done(); - }) - .catch(done.fail); - }); - - it('creates temp file', (done) => { - const projectTree = store.state.trees['abcproject/mybranch']; - - store.dispatch('createTempEntry', { - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - name: 'test', - type: 'blob', - }) - .then(() => { - const baseTree = projectTree.tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].tempFile).toBeTruthy(); - expect(baseTree[0].type).toBe('blob'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('popHistoryState', () => { - - }); - - describe('scrollToTab', () => { - it('focuses the current active element', (done) => { - document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>'; - const el = document.querySelector('.repo-tab'); - spyOn(el, 'focus'); - - store.dispatch('scrollToTab') - .then(() => { - setTimeout(() => { - expect(el.focus).toHaveBeenCalled(); - - document.getElementById('tabs').remove(); - - done(); - }); - }) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js deleted file mode 100644 index d0d5934f29a..00000000000 --- a/spec/javascripts/repo/stores/getters_spec.js +++ /dev/null @@ -1,114 +0,0 @@ -import * as getters from '~/ide/stores/getters'; -import state from '~/ide/stores/state'; -import { file } from '../helpers'; - -describe('Multi-file store getters', () => { - let localState; - - beforeEach(() => { - localState = state(); - }); - - describe('changedFiles', () => { - it('returns a list of changed opened files', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('changed')); - localState.openFiles[1].changed = true; - - const changedFiles = getters.changedFiles(localState); - - expect(changedFiles.length).toBe(1); - expect(changedFiles[0].name).toBe('changed'); - }); - }); - - describe('activeFile', () => { - it('returns the current active file', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('active')); - localState.openFiles[1].active = true; - - expect(getters.activeFile(localState).name).toBe('active'); - }); - - it('returns undefined if no active files are found', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('active')); - - expect(getters.activeFile(localState)).toBeNull(); - }); - }); - - describe('activeFileExtension', () => { - it('returns the file extension for the current active file', () => { - localState.openFiles.push(file('active')); - localState.openFiles[0].active = true; - localState.openFiles[0].path = 'test.js'; - - expect(getters.activeFileExtension(localState)).toBe('.js'); - - localState.openFiles[0].path = 'test.es6.js'; - - expect(getters.activeFileExtension(localState)).toBe('.js'); - }); - }); - - describe('canEditFile', () => { - beforeEach(() => { - localState.onTopOfBranch = true; - localState.canCommit = true; - - localState.openFiles.push(file()); - localState.openFiles[0].active = true; - }); - - it('returns true if user can commit and has open files', () => { - expect(getters.canEditFile(localState)).toBeTruthy(); - }); - - it('returns false if user can commit and has no open files', () => { - localState.openFiles = []; - - expect(getters.canEditFile(localState)).toBeFalsy(); - }); - - it('returns false if user can commit and active file is binary', () => { - localState.openFiles[0].binary = true; - - expect(getters.canEditFile(localState)).toBeFalsy(); - }); - - it('returns false if user cant commit', () => { - localState.canCommit = false; - - expect(getters.canEditFile(localState)).toBeFalsy(); - }); - }); - - describe('modifiedFiles', () => { - it('returns a list of modified files', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('changed')); - localState.openFiles[1].changed = true; - - const modifiedFiles = getters.modifiedFiles(localState); - - expect(modifiedFiles.length).toBe(1); - expect(modifiedFiles[0].name).toBe('changed'); - }); - }); - - describe('addedFiles', () => { - it('returns a list of added files', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('added')); - localState.openFiles[1].changed = true; - localState.openFiles[1].tempFile = true; - - const modifiedFiles = getters.addedFiles(localState); - - expect(modifiedFiles.length).toBe(1); - expect(modifiedFiles[0].name).toBe('added'); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js deleted file mode 100644 index a7167537ef2..00000000000 --- a/spec/javascripts/repo/stores/mutations/branch_spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import mutations from '~/ide/stores/mutations/branch'; -import state from '~/ide/stores/state'; - -describe('Multi-file store branch mutations', () => { - let localState; - - beforeEach(() => { - localState = state(); - }); - - describe('SET_CURRENT_BRANCH', () => { - it('sets currentBranch', () => { - mutations.SET_CURRENT_BRANCH(localState, 'master'); - - expect(localState.currentBranchId).toBe('master'); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js deleted file mode 100644 index 6e204ef0404..00000000000 --- a/spec/javascripts/repo/stores/mutations/file_spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import mutations from '~/ide/stores/mutations/file'; -import state from '~/ide/stores/state'; -import { file } from '../../helpers'; - -describe('Multi-file store file mutations', () => { - let localState; - let localFile; - - beforeEach(() => { - localState = state(); - localFile = file(); - }); - - describe('SET_FILE_ACTIVE', () => { - it('sets the file active', () => { - mutations.SET_FILE_ACTIVE(localState, { - file: localFile, - active: true, - }); - - expect(localFile.active).toBeTruthy(); - }); - }); - - describe('TOGGLE_FILE_OPEN', () => { - beforeEach(() => { - mutations.TOGGLE_FILE_OPEN(localState, localFile); - }); - - it('adds into opened files', () => { - expect(localFile.opened).toBeTruthy(); - expect(localState.openFiles.length).toBe(1); - }); - - it('removes from opened files', () => { - mutations.TOGGLE_FILE_OPEN(localState, localFile); - - expect(localFile.opened).toBeFalsy(); - expect(localState.openFiles.length).toBe(0); - }); - }); - - describe('SET_FILE_DATA', () => { - it('sets extra file data', () => { - mutations.SET_FILE_DATA(localState, { - data: { - blame_path: 'blame', - commits_path: 'commits', - permalink: 'permalink', - raw_path: 'raw', - binary: true, - html: 'html', - render_error: 'render_error', - }, - file: localFile, - }); - - expect(localFile.blamePath).toBe('blame'); - expect(localFile.commitsPath).toBe('commits'); - expect(localFile.permalink).toBe('permalink'); - expect(localFile.rawPath).toBe('raw'); - expect(localFile.binary).toBeTruthy(); - expect(localFile.html).toBe('html'); - expect(localFile.renderError).toBe('render_error'); - }); - }); - - describe('SET_FILE_RAW_DATA', () => { - it('sets raw data', () => { - mutations.SET_FILE_RAW_DATA(localState, { - file: localFile, - raw: 'testing', - }); - - expect(localFile.raw).toBe('testing'); - }); - }); - - describe('UPDATE_FILE_CONTENT', () => { - beforeEach(() => { - localFile.raw = 'test'; - }); - - it('sets content', () => { - mutations.UPDATE_FILE_CONTENT(localState, { - file: localFile, - content: 'test', - }); - - expect(localFile.content).toBe('test'); - }); - - it('sets changed if content does not match raw', () => { - mutations.UPDATE_FILE_CONTENT(localState, { - file: localFile, - content: 'testing', - }); - - expect(localFile.content).toBe('testing'); - expect(localFile.changed).toBeTruthy(); - }); - }); - - describe('DISCARD_FILE_CHANGES', () => { - beforeEach(() => { - localFile.content = 'test'; - localFile.changed = true; - }); - - it('resets content and changed', () => { - mutations.DISCARD_FILE_CHANGES(localState, localFile); - - expect(localFile.content).toBe(''); - expect(localFile.changed).toBeFalsy(); - }); - }); - - describe('CREATE_TMP_FILE', () => { - it('adds file into parent tree', () => { - const f = file('tmpFile'); - - mutations.CREATE_TMP_FILE(localState, { - file: f, - parent: localFile, - }); - - expect(localFile.tree.length).toBe(1); - expect(localFile.tree[0].name).toBe(f.name); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js deleted file mode 100644 index e6ca8ea139e..00000000000 --- a/spec/javascripts/repo/stores/mutations/tree_spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import mutations from '~/ide/stores/mutations/tree'; -import state from '~/ide/stores/state'; -import { file } from '../../helpers'; - -describe('Multi-file store tree mutations', () => { - let localState; - let localTree; - - beforeEach(() => { - localState = state(); - localTree = file(); - }); - - describe('TOGGLE_TREE_OPEN', () => { - it('toggles tree open', () => { - mutations.TOGGLE_TREE_OPEN(localState, localTree); - - expect(localTree.opened).toBeTruthy(); - - mutations.TOGGLE_TREE_OPEN(localState, localTree); - - expect(localTree.opened).toBeFalsy(); - }); - }); - - describe('SET_DIRECTORY_DATA', () => { - const data = [{ - name: 'tree', - }, - { - name: 'submodule', - }, - { - name: 'blob', - }]; - - it('adds directory data', () => { - mutations.SET_DIRECTORY_DATA(localState, { - data, - tree: localState, - }); - - expect(localState.tree.length).toBe(3); - expect(localState.tree[0].name).toBe('tree'); - expect(localState.tree[1].name).toBe('submodule'); - expect(localState.tree[2].name).toBe('blob'); - }); - }); - - describe('SET_PARENT_TREE_URL', () => { - it('sets the parent tree url', () => { - mutations.SET_PARENT_TREE_URL(localState, 'test'); - - expect(localState.parentTreeUrl).toBe('test'); - }); - }); - - describe('CREATE_TMP_TREE', () => { - it('adds tree into parent tree', () => { - const tmpEntry = file('tmpTree'); - - mutations.CREATE_TMP_TREE(localState, { - tmpEntry, - parent: localTree, - }); - - expect(localTree.tree.length).toBe(1); - expect(localTree.tree[0].name).toBe(tmpEntry.name); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js deleted file mode 100644 index 5fd8ad94972..00000000000 --- a/spec/javascripts/repo/stores/mutations_spec.js +++ /dev/null @@ -1,125 +0,0 @@ -import mutations from '~/ide/stores/mutations'; -import state from '~/ide/stores/state'; -import { file } from '../helpers'; - -describe('Multi-file store mutations', () => { - let localState; - let entry; - - beforeEach(() => { - localState = state(); - entry = file(); - }); - - describe('SET_INITIAL_DATA', () => { - it('sets all initial data', () => { - mutations.SET_INITIAL_DATA(localState, { - test: 'test', - }); - - expect(localState.test).toBe('test'); - }); - }); - - describe('SET_PREVIEW_MODE', () => { - it('sets currentBlobView to repo-preview', () => { - mutations.SET_PREVIEW_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-preview'); - - localState.currentBlobView = 'testing'; - - mutations.SET_PREVIEW_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-preview'); - }); - }); - - describe('SET_EDIT_MODE', () => { - it('sets currentBlobView to repo-editor', () => { - mutations.SET_EDIT_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-editor'); - - localState.currentBlobView = 'testing'; - - mutations.SET_EDIT_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-editor'); - }); - }); - - describe('TOGGLE_LOADING', () => { - it('toggles loading of entry', () => { - mutations.TOGGLE_LOADING(localState, entry); - - expect(entry.loading).toBeTruthy(); - - mutations.TOGGLE_LOADING(localState, entry); - - expect(entry.loading).toBeFalsy(); - }); - }); - - describe('TOGGLE_EDIT_MODE', () => { - it('toggles editMode', () => { - mutations.TOGGLE_EDIT_MODE(localState); - - expect(localState.editMode).toBeFalsy(); - - mutations.TOGGLE_EDIT_MODE(localState); - - expect(localState.editMode).toBeTruthy(); - }); - }); - - describe('TOGGLE_DISCARD_POPUP', () => { - it('sets discardPopupOpen', () => { - mutations.TOGGLE_DISCARD_POPUP(localState, true); - - expect(localState.discardPopupOpen).toBeTruthy(); - - mutations.TOGGLE_DISCARD_POPUP(localState, false); - - expect(localState.discardPopupOpen).toBeFalsy(); - }); - }); - - describe('SET_ROOT', () => { - it('sets isRoot & initialRoot', () => { - mutations.SET_ROOT(localState, true); - - expect(localState.isRoot).toBeTruthy(); - expect(localState.isInitialRoot).toBeTruthy(); - - mutations.SET_ROOT(localState, false); - - expect(localState.isRoot).toBeFalsy(); - expect(localState.isInitialRoot).toBeFalsy(); - }); - }); - - describe('SET_LEFT_PANEL_COLLAPSED', () => { - it('sets left panel collapsed', () => { - mutations.SET_LEFT_PANEL_COLLAPSED(localState, true); - - expect(localState.leftPanelCollapsed).toBeTruthy(); - - mutations.SET_LEFT_PANEL_COLLAPSED(localState, false); - - expect(localState.leftPanelCollapsed).toBeFalsy(); - }); - }); - - describe('SET_RIGHT_PANEL_COLLAPSED', () => { - it('sets right panel collapsed', () => { - mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true); - - expect(localState.rightPanelCollapsed).toBeTruthy(); - - mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false); - - expect(localState.rightPanelCollapsed).toBeFalsy(); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js deleted file mode 100644 index 89745a2029e..00000000000 --- a/spec/javascripts/repo/stores/utils_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import * as utils from '~/ide/stores/utils'; -import state from '~/ide/stores/state'; -import { file } from '../helpers'; - -describe('Multi-file store utils', () => { - describe('setPageTitle', () => { - it('sets the document page title', () => { - utils.setPageTitle('test'); - - expect(document.title).toBe('test'); - }); - }); - - describe('treeList', () => { - let localState; - - beforeEach(() => { - localState = state(); - }); - - it('returns flat tree list', () => { - localState.trees = []; - localState.trees['abcproject/mybranch'] = { - tree: [], - }; - const baseTree = localState.trees['abcproject/mybranch'].tree; - baseTree.push(file('1')); - baseTree[0].tree.push(file('2')); - baseTree[0].tree[0].tree.push(file('3')); - - const treeList = utils.treeList(localState, 'abcproject/mybranch'); - - expect(treeList.length).toBe(3); - expect(treeList[1].name).toBe(baseTree[0].tree[0].name); - expect(treeList[2].name).toBe(baseTree[0].tree[0].tree[0].name); - }); - }); - - describe('createTemp', () => { - it('creates temp tree', () => { - const tmp = utils.createTemp({ - name: 'test', - path: 'test', - type: 'tree', - level: 0, - changed: false, - content: '', - base64: '', - }); - - expect(tmp.tempFile).toBeTruthy(); - expect(tmp.icon).toBe('fa-folder'); - }); - - it('creates temp file', () => { - const tmp = utils.createTemp({ - name: 'test', - path: 'test', - type: 'blob', - level: 0, - changed: false, - content: '', - base64: '', - }); - - expect(tmp.tempFile).toBeTruthy(); - expect(tmp.icon).toBe('fa-file-text-o'); - }); - }); - - describe('findIndexOfFile', () => { - let localState; - - beforeEach(() => { - localState = [{ - path: '1', - }, { - path: '2', - }]; - }); - - it('finds in the index of an entry by path', () => { - const index = utils.findIndexOfFile(localState, { - path: '2', - }); - - expect(index).toBe(1); - }); - }); - - describe('findEntry', () => { - let localState; - - beforeEach(() => { - localState = { - tree: [{ - type: 'tree', - name: 'test', - }, { - type: 'blob', - name: 'file', - }], - }; - }); - - it('returns an entry found by name', () => { - const foundEntry = utils.findEntry(localState.tree, 'tree', 'test'); - - expect(foundEntry.type).toBe('tree'); - expect(foundEntry.name).toBe('test'); - }); - - it('returns undefined when no entry found', () => { - const foundEntry = utils.findEntry(localState.tree, 'blob', 'test'); - - expect(foundEntry).toBeUndefined(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js new file mode 100644 index 00000000000..67056793a20 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; + +import LabelsSelect from '~/labels_select'; +import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (config = mockConfig) => { + const Component = Vue.extend(baseComponent); + + return mountComponent(Component, config); +}; + +describe('BaseComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('hiddenInputName', () => { + it('returns correct string when showCreate prop is `true`', () => { + expect(vm.hiddenInputName).toBe('issue[label_names][]'); + }); + + it('returns correct string when showCreate prop is `false`', () => { + const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false }); + const vmNonEditable = createComponent(mockConfigNonEditable); + expect(vmNonEditable.hiddenInputName).toBe('label_id[]'); + vmNonEditable.$destroy(); + }); + }); + }); + + describe('methods', () => { + describe('handleClick', () => { + it('emits onLabelClick event with label and list of labels as params', () => { + spyOn(vm, '$emit'); + vm.handleClick(mockLabels[0]); + expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]); + }); + }); + }); + + describe('mounted', () => { + it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => { + expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true); + }); + }); + + describe('template', () => { + it('renders component container element with classes `block labels`', () => { + expect(vm.$el.classList.contains('block')).toBe(true); + expect(vm.$el.classList.contains('labels')).toBe(true); + }); + + it('renders `.selectbox` element', () => { + expect(vm.$el.querySelector('.selectbox')).not.toBeNull(); + expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;'); + }); + + it('renders `.dropdown` element', () => { + expect(vm.$el.querySelector('.dropdown')).not.toBeNull(); + }); + + it('renders `.dropdown-menu` element', () => { + const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu'); + expect(dropdownMenuEl).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js new file mode 100644 index 00000000000..ec63ac306d0 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; + +import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const componentConfig = Object.assign({}, mockConfig, { + fieldName: 'label_id[]', + labels: mockLabels, + showExtraOptions: false, +}); + +const createComponent = (config = componentConfig) => { + const Component = Vue.extend(dropdownButtonComponent); + + return mountComponent(Component, config); +}; + +describe('DropdownButtonComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('dropdownToggleText', () => { + it('returns text as `Label` when `labels` prop is empty array', () => { + const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] }); + const vmEmptyLabels = createComponent(mockEmptyLabels); + expect(vmEmptyLabels.dropdownToggleText).toBe('Label'); + vmEmptyLabels.$destroy(); + }); + + it('returns first label name with remaining label count when `labels` prop has more than one item', () => { + const mockMoreLabels = Object.assign({}, componentConfig, { + labels: mockLabels.concat(mockLabels), + }); + const vmMoreLabels = createComponent(mockMoreLabels); + expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more'); + vmMoreLabels.$destroy(); + }); + + it('returns first label name when `labels` prop has only one item present', () => { + expect(vm.dropdownToggleText).toBe('Foo Label'); + }); + }); + }); + + describe('template', () => { + it('renders component container element of type `button`', () => { + expect(vm.$el.nodeName).toBe('BUTTON'); + }); + + it('renders component container element with required data attributes', () => { + expect(vm.$el.dataset.abilityName).toBe(vm.abilityName); + expect(vm.$el.dataset.fieldName).toBe(vm.fieldName); + expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath); + expect(vm.$el.dataset.labels).toBe(vm.labelsPath); + expect(vm.$el.dataset.namespacePath).toBe(vm.namespace); + expect(vm.$el.dataset.showAny).not.toBeDefined(); + }); + + it('renders dropdown toggle text element', () => { + const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text'); + expect(dropdownToggleTextEl).not.toBeNull(); + expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label'); + }); + + it('renders dropdown button icon', () => { + const dropdownIconEl = vm.$el.querySelector('i.fa'); + expect(dropdownIconEl).not.toBeNull(); + expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js new file mode 100644 index 00000000000..f07aefb2f87 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; + +import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue'; + +import { mockSuggestedColors } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownCreateLabelComponent); + + return mountComponent(Component); +}; + +describe('DropdownCreateLabelComponent', () => { + let vm; + + beforeEach(() => { + gon.suggested_label_colors = mockSuggestedColors; + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('created', () => { + it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => { + expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length); + }); + }); + + describe('template', () => { + it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => { + expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true); + }); + + it('renders `Go back` button on component header', () => { + const backButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-back'); + expect(backButtonEl).not.toBe(null); + expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null); + }); + + it('renders component header element', () => { + const headerEl = vm.$el.querySelector('.dropdown-title'); + expect(headerEl.innerText.trim()).toContain('Create new label'); + }); + + it('renders `Close` button on component header', () => { + const closeButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close'); + expect(closeButtonEl).not.toBe(null); + expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null); + }); + + it('renders `Name new label` input element', () => { + expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null); + expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null); + }); + + it('renders suggested colors list elements', () => { + const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown'); + expect(colorsListContainerEl).not.toBe(null); + expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length); + + const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0]; + expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]); + expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);'); + }); + + it('renders color input element', () => { + expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null); + expect(vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview')).not.toBe(null); + expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null); + }); + + it('renders component action buttons', () => { + const createBtnEl = vm.$el.querySelector('button.js-new-label-btn'); + const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn'); + expect(createBtnEl).not.toBe(null); + expect(createBtnEl.innerText.trim()).toBe('Create'); + expect(cancelBtnEl.innerText.trim()).toBe('Cancel'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js new file mode 100644 index 00000000000..809e0327b1c --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; + +import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue'; + +import { mockConfig } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (labelsWebUrl = mockConfig.labelsWebUrl) => { + const Component = Vue.extend(dropdownFooterComponent); + + return mountComponent(Component, { + labelsWebUrl, + }); +}; + +describe('DropdownFooterComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders `Create new label` link element', () => { + const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page'); + expect(createLabelEl).not.toBeNull(); + expect(createLabelEl.innerText.trim()).toBe('Create new label'); + }); + + it('renders `Manage labels` link element', () => { + const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link'); + expect(manageLabelsEl).not.toBeNull(); + expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl); + expect(manageLabelsEl.innerText.trim()).toBe('Manage labels'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js new file mode 100644 index 00000000000..325fa47c957 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; + +import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownHeaderComponent); + + return mountComponent(Component); +}; + +describe('DropdownHeaderComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders header text element', () => { + const headerEl = vm.$el.querySelector('.dropdown-title span'); + expect(headerEl.innerText.trim()).toBe('Assign labels'); + }); + + it('renders `Close` button element', () => { + const closeBtnEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close'); + expect(closeBtnEl).not.toBeNull(); + expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js new file mode 100644 index 00000000000..703b87498c7 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; + +import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue'; + +import { mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (name = 'label_id[]', label = mockLabels[0]) => { + const Component = Vue.extend(dropdownHiddenInputComponent); + + return mountComponent(Component, { + name, + label, + }); +}; + +describe('DropdownHiddenInputComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders input element of type `hidden`', () => { + expect(vm.$el.nodeName).toBe('INPUT'); + expect(vm.$el.getAttribute('type')).toBe('hidden'); + expect(vm.$el.getAttribute('name')).toBe(vm.name); + expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js new file mode 100644 index 00000000000..69e11d966c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; + +import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownSearchInputComponent); + + return mountComponent(Component); +}; + +describe('DropdownSearchInputComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders input element with type `search`', () => { + const inputEl = vm.$el.querySelector('input.dropdown-input-field'); + expect(inputEl).not.toBeNull(); + expect(inputEl.getAttribute('type')).toBe('search'); + }); + + it('renders search icon element', () => { + expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull(); + }); + + it('renders clear search icon element', () => { + expect(vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js new file mode 100644 index 00000000000..c3580933072 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; + +import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (canEdit = true) => { + const Component = Vue.extend(dropdownTitleComponent); + + return mountComponent(Component, { + canEdit, + }); +}; + +describe('DropdownTitleComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders title text', () => { + expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true); + expect(vm.$el.innerText.trim()).toContain('Labels'); + }); + + it('renders spinner icon element', () => { + expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull(); + }); + + it('renders `Edit` button element', () => { + const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle'); + expect(editBtnEl).not.toBeNull(); + expect(editBtnEl.innerText.trim()).toBe('Edit'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js new file mode 100644 index 00000000000..93b42795bea --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -0,0 +1,74 @@ +import Vue from 'vue'; + +import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; + +import { mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (labels = mockLabels) => { + const Component = Vue.extend(dropdownValueCollapsedComponent); + + return mountComponent(Component, { + labels, + }); +}; + +describe('DropdownValueCollapsedComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('labelsList', () => { + it('returns empty text when `labels` prop is empty array', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.labelsList).toBe(''); + vmEmptyLabels.$destroy(); + }); + + it('returns labels names separated by coma when `labels` prop has more than one item', () => { + const vmMoreLabels = createComponent(mockLabels.concat(mockLabels)); + expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label'); + vmMoreLabels.$destroy(); + }); + + it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => { + const mockMoreLabels = Object.assign([], mockLabels); + for (let i = 0; i < 6; i += 1) { + mockMoreLabels.unshift(mockLabels[0]); + } + + const vmMoreLabels = createComponent(mockMoreLabels); + expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more'); + vmMoreLabels.$destroy(); + }); + + it('returns first label name when `labels` prop has only one item present', () => { + expect(vm.labelsList).toBe('Foo Label'); + }); + }); + }); + + describe('template', () => { + it('renders component container element with tooltip`', () => { + expect(vm.$el.dataset.placement).toBe('left'); + expect(vm.$el.dataset.container).toBe('body'); + expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList); + }); + + it('renders tags icon element', () => { + expect(vm.$el.querySelector('.fa-tags')).not.toBeNull(); + }); + + it('renders labels count', () => { + expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js new file mode 100644 index 00000000000..66e0957b431 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -0,0 +1,94 @@ +import Vue from 'vue'; + +import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = ( + labels = mockLabels, + labelFilterBasePath = mockConfig.labelFilterBasePath, +) => { + const Component = Vue.extend(dropdownValueComponent); + + return mountComponent(Component, { + labels, + labelFilterBasePath, + }); +}; + +describe('DropdownValueComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('isEmpty', () => { + it('returns true if `labels` prop is empty', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.isEmpty).toBe(true); + vmEmptyLabels.$destroy(); + }); + + it('returns false if `labels` prop is empty', () => { + expect(vm.isEmpty).toBe(false); + }); + }); + }); + + describe('methods', () => { + describe('labelFilterUrl', () => { + it('returns URL string starting with labelFilterBasePath and encoded label.title', () => { + expect(vm.labelFilterUrl({ + title: 'Foo bar', + })).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar'); + }); + }); + + describe('labelStyle', () => { + it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => { + const label = { + textColor: '#FFFFFF', + color: '#BADA55', + }; + const styleObj = vm.labelStyle(label); + + expect(styleObj.color).toBe(label.textColor); + expect(styleObj.backgroundColor).toBe(label.color); + }); + }); + }); + + describe('template', () => { + it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => { + expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(true); + }); + + it('render slot content inside component when `labels` prop is empty', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(mockConfig.emptyValueText); + vmEmptyLabels.$destroy(); + }); + + it('renders label element with filter URL', () => { + expect(vm.$el.querySelector('a').getAttribute('href')).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20Label'); + }); + + it('renders label element with tooltip and styles based on label details', () => { + const labelEl = vm.$el.querySelector('a span.label.color-label'); + expect(labelEl).not.toBeNull(); + expect(labelEl.dataset.placement).toBe('bottom'); + expect(labelEl.dataset.container).toBe('body'); + expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description); + expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);'); + expect(labelEl.innerText.trim()).toBe(mockLabels[0].title); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js new file mode 100644 index 00000000000..e9008c29b22 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js @@ -0,0 +1,49 @@ +export const mockLabels = [ + { + id: 26, + title: 'Foo Label', + description: 'Foobar', + color: '#BADA55', + text_color: '#FFFFFF', + }, +]; + +export const mockSuggestedColors = [ + '#0033CC', + '#428BCA', + '#44AD8E', + '#A8D695', + '#5CB85C', + '#69D100', + '#004E00', + '#34495E', + '#7F8C8D', + '#A295D6', + '#5843AD', + '#8E44AD', + '#FFECDB', + '#AD4363', + '#D10069', + '#CC0033', + '#FF0000', + '#D9534F', + '#D1D100', + '#F0AD4E', + '#AD8D43', +]; + +export const mockConfig = { + showCreate: true, + abilityName: 'issue', + context: { + labels: mockLabels, + }, + namespace: 'gitlab-org', + updatePath: '/gitlab-org/my-project/issue/1', + labelsPath: '/gitlab-org/my-project/labels.json', + labelsWebUrl: '/gitlab-org/my-project/labels', + labelFilterBasePath: '/gitlab-org/my-project/issues', + canEdit: true, + suggestedColors: mockSuggestedColors, + emptyValueText: 'None', +}; diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index f7b1a61f4f8..a9b5ed1112a 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -28,6 +28,23 @@ describe Backup::Repository do end describe '#restore' do + subject { described_class.new } + + let(:timestamp) { Time.utc(2017, 3, 22) } + let(:temp_dirs) do + Gitlab.config.repositories.storages.map do |name, storage| + File.join(storage['path'], '..', 'repositories.old.' + timestamp.to_i.to_s) + end + end + + around do |example| + Timecop.freeze(timestamp) { example.run } + end + + after do + temp_dirs.each { |path| FileUtils.rm_rf(path) } + end + describe 'command failure' do before do allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) @@ -35,7 +52,7 @@ describe Backup::Repository do context 'hashed storage' do it 'shows the appropriate error' do - described_class.new.restore + subject.restore expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} (#{project.disk_path}) - error") end @@ -45,7 +62,7 @@ describe Backup::Repository do let!(:project) { create(:project, :legacy_storage) } it 'shows the appropriate error' do - described_class.new.restore + subject.restore expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") end diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index b7c2ff03125..b502daea418 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -4,6 +4,7 @@ describe Banzai::Filter::AutolinkFilter do include FilterSpecHelper let(:link) { 'http://about.gitlab.com/' } + let(:quotes) { ['"', "'"] } it 'does nothing when :autolink is false' do exp = act = link @@ -15,17 +16,7 @@ describe Banzai::Filter::AutolinkFilter do expect(filter(act).to_html).to eq exp end - context 'when the input contains no links' do - it 'does not parse_html back the rinku returned value' do - act = HTML::Pipeline.parse('<p>This text contains no links to autolink</p>') - - expect_any_instance_of(described_class).not_to receive(:parse_html) - - filter(act).to_html - end - end - - context 'Rinku schemes' do + context 'Various schemes' do it 'autolinks http' do doc = filter("See #{link}") expect(doc.at_css('a').text).to eq link @@ -56,32 +47,26 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a')['href']).to eq link end - it 'accepts link_attr options' do - doc = filter("See #{link}", link_attr: { class: 'custom' }) + it 'autolinks multiple URLs' do + link1 = 'http://localhost:3000/' + link2 = 'http://google.com/' - expect(doc.at_css('a')['class']).to eq 'custom' - end + doc = filter("See #{link1} and #{link2}") - described_class::IGNORE_PARENTS.each do |elem| - it "ignores valid links contained inside '#{elem}' element" do - exp = act = "<#{elem}>See #{link}</#{elem}>" - expect(filter(act).to_html).to eq exp - end - end + found_links = doc.css('a') - context 'when the input contains link' do - it 'does parse_html back the rinku returned value' do - act = HTML::Pipeline.parse("<p>See #{link}</p>") + expect(found_links.size).to eq(2) + expect(found_links[0].text).to eq(link1) + expect(found_links[0]['href']).to eq(link1) + expect(found_links[1].text).to eq(link2) + expect(found_links[1]['href']).to eq(link2) + end - expect_any_instance_of(described_class).to receive(:parse_html).at_least(:once).and_call_original + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: { class: 'custom' }) - filter(act).to_html - end + expect(doc.at_css('a')['class']).to eq 'custom' end - end - - context 'other schemes' do - let(:link) { 'foo://bar.baz/' } it 'autolinks smb' do link = 'smb:///Volumes/shared/foo.pdf' @@ -91,6 +76,21 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a')['href']).to eq link end + it 'autolinks multiple occurences of smb' do + link1 = 'smb:///Volumes/shared/foo.pdf' + link2 = 'smb:///Volumes/shared/bar.pdf' + + doc = filter("See #{link1} and #{link2}") + + found_links = doc.css('a') + + expect(found_links.size).to eq(2) + expect(found_links[0].text).to eq(link1) + expect(found_links[0]['href']).to eq(link1) + expect(found_links[1].text).to eq(link2) + expect(found_links[1]['href']).to eq(link2) + end + it 'autolinks irc' do link = 'irc://irc.freenode.net/git' doc = filter("See #{link}") @@ -132,6 +132,45 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a').text).to eq link end + it 'includes trailing punctuation when part of a balanced pair' do + described_class::PUNCTUATION_PAIRS.each do |close, open| + next if open.in?(quotes) + + balanced_link = "#{link}#{open}abc#{close}" + balanced_actual = filter("See #{balanced_link}...") + unbalanced_link = "#{link}#{close}" + unbalanced_actual = filter("See #{unbalanced_link}...") + + expect(balanced_actual.at_css('a').text).to eq(balanced_link) + expect(unescape(balanced_actual.to_html)).to eq(Rinku.auto_link("See #{balanced_link}...")) + expect(unbalanced_actual.at_css('a').text).to eq(link) + expect(unescape(unbalanced_actual.to_html)).to eq(Rinku.auto_link("See #{unbalanced_link}...")) + end + end + + it 'removes trailing quotes' do + quotes.each do |quote| + balanced_link = "#{link}#{quote}abc#{quote}" + balanced_actual = filter("See #{balanced_link}...") + unbalanced_link = "#{link}#{quote}" + unbalanced_actual = filter("See #{unbalanced_link}...") + + expect(balanced_actual.at_css('a').text).to eq(balanced_link[0...-1]) + expect(unescape(balanced_actual.to_html)).to eq(Rinku.auto_link("See #{balanced_link}...")) + expect(unbalanced_actual.at_css('a').text).to eq(link) + expect(unescape(unbalanced_actual.to_html)).to eq(Rinku.auto_link("See #{unbalanced_link}...")) + end + end + + it 'removes one closing punctuation mark when the punctuation in the link is unbalanced' do + complicated_link = "(#{link}(a'b[c'd]))'" + expected_complicated_link = %Q{(<a href="#{link}(a'b[c'd]))">#{link}(a'b[c'd]))</a>'} + actual = unescape(filter(complicated_link).to_html) + + expect(actual).to eq(Rinku.auto_link(complicated_link)) + expect(actual).to eq(expected_complicated_link) + end + it 'does not include trailing HTML entities' do doc = filter("See <<<#{link}>>>") @@ -151,4 +190,29 @@ describe Banzai::Filter::AutolinkFilter do end end end + + context 'when the link is inside a tag' do + %w[http rdar].each do |protocol| + it "renders text after the link correctly for #{protocol}" do + doc = filter(ERB::Util.html_escape_once("<#{protocol}://link><another>")) + + expect(doc.children.last.text).to include('<another>') + end + end + end + + # Rinku does not escape these characters in HTML attributes, but content_tag + # does. We don't care about that difference for these specs, though. + def unescape(html) + %w([ ] { }).each do |cgi_escape| + html.sub!(CGI.escape(cgi_escape), cgi_escape) + end + + quotes.each do |html_escape| + html.sub!(CGI.escape_html(html_escape), html_escape) + html.sub!(CGI.escape(html_escape), CGI.escape_html(html_escape)) + end + + html + end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 862b1fe3fd3..0c524a1551f 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -381,11 +381,11 @@ describe Banzai::Filter::LabelReferenceFilter do end it 'has valid link text' do - expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}" + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.full_name}" end it 'has valid text' do - expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}" + expect(result.text).to eq "See #{label.name} in #{project2.full_name}" end it 'ignores invalid IDs on the referenced label' do @@ -481,12 +481,12 @@ describe Banzai::Filter::LabelReferenceFilter do it 'has valid link text' do expect(result.css('a').first.text) - .to eq "#{group_label.name} in #{another_project.name_with_namespace}" + .to eq "#{group_label.name} in #{another_project.full_name}" end it 'has valid text' do expect(result.text) - .to eq "See #{group_label.name} in #{another_project.name_with_namespace}" + .to eq "See #{group_label.name} in #{another_project.full_name}" end it 'ignores invalid IDs on the referenced label' do diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb index 17756621221..7201e4f7bf6 100644 --- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb +++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb @@ -2,23 +2,25 @@ require 'spec_helper' describe Gitlab::Checks::LfsIntegrity do include ProjectForksHelper + let(:project) { create(:project, :repository) } - let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:repository) { project.repository } + let(:newrev) do + operations = BareRepoOperations.new(repository.path) + + # Create a commit not pointed at by any ref to emulate being in the + # pre-receive hook so that `--not --all` returns some objects + operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects") + end subject { described_class.new(project, newrev) } describe '#objects_missing?' do - let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } - - before do - allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block| - lazy_block.call([blob_object.id]) - end - end + let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } context 'with LFS not enabled' do it 'skips integrity check' do - expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) + expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers) subject.objects_missing? end @@ -33,7 +35,7 @@ describe Gitlab::Checks::LfsIntegrity do let(:newrev) { nil } it 'skips integrity check' do - expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) + expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers) expect(subject.objects_missing?).to be_falsey end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 91c9625ba06..1c73043cfbd 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -399,4 +399,138 @@ describe Gitlab::Ci::Trace do end end end + + describe '#archive!' do + subject { trace.archive! } + + shared_examples 'archive trace file' do + it do + expect { subject }.to change { Ci::JobArtifact.count }.by(1) + + build.reload + expect(build.trace.exist?).to be_truthy + expect(build.job_artifacts_trace.file.exists?).to be_truthy + expect(build.job_artifacts_trace.file.filename).to eq('job.log') + expect(File.exist?(src_path)).to be_falsy + expect(src_checksum) + .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).digest) + end + end + + shared_examples 'source trace file stays intact' do |error:| + it do + expect { subject }.to raise_error(error) + + build.reload + expect(build.trace.exist?).to be_truthy + expect(build.job_artifacts_trace).to be_nil + expect(File.exist?(src_path)).to be_truthy + end + end + + shared_examples 'archive trace in database' do + it do + expect { subject }.to change { Ci::JobArtifact.count }.by(1) + + build.reload + expect(build.trace.exist?).to be_truthy + expect(build.job_artifacts_trace.file.exists?).to be_truthy + expect(build.job_artifacts_trace.file.filename).to eq('job.log') + expect(build.old_trace).to be_nil + expect(src_checksum) + .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).digest) + end + end + + shared_examples 'source trace in database stays intact' do |error:| + it do + expect { subject }.to raise_error(error) + + build.reload + expect(build.trace.exist?).to be_truthy + expect(build.job_artifacts_trace).to be_nil + expect(build.old_trace).to eq(trace_content) + end + end + + context 'when job does not have trace artifact' do + context 'when trace file stored in default path' do + let!(:build) { create(:ci_build, :success, :trace_live) } + let!(:src_path) { trace.read { |s| return s.path } } + let!(:src_checksum) { Digest::SHA256.file(src_path).digest } + + it_behaves_like 'archive trace file' + + context 'when failed to create clone file' do + before do + allow(IO).to receive(:copy_stream).and_return(0) + end + + it_behaves_like 'source trace file stays intact', error: Gitlab::Ci::Trace::ArchiveError + end + + context 'when failed to create job artifact record' do + before do + allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false) + allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages) + .and_return(%w[Error Error]) + end + + it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid + end + end + + context 'when trace is stored in database' do + let(:build) { create(:ci_build, :success) } + let(:trace_content) { 'Sample trace' } + let!(:src_checksum) { Digest::SHA256.digest(trace_content) } + + before do + build.update_column(:trace, trace_content) + end + + it_behaves_like 'archive trace in database' + + context 'when failed to create clone file' do + before do + allow(IO).to receive(:copy_stream).and_return(0) + end + + it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError + end + + context 'when failed to create job artifact record' do + before do + allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false) + allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages) + .and_return(%w[Error Error]) + end + + it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid + end + end + end + + context 'when job has trace artifact' do + before do + create(:ci_job_artifact, :trace, job: build) + end + + it 'does not archive' do + expect_any_instance_of(described_class).not_to receive(:archive_stream!) + expect { subject }.to raise_error('Already archived') + expect(build.job_artifacts_trace.file.exists?).to be_truthy + end + end + + context 'when job is not finished yet' do + let!(:build) { create(:ci_build, :running, :trace_live) } + + it 'does not archive' do + expect_any_instance_of(described_class).not_to receive(:archive_stream!) + expect { subject }.to raise_error('Job is not finished yet') + expect(build.trace.exist?).to be_truthy + end + end + end end diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 49a179ba875..167876ca158 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::ContributionsCalendar do end let(:public_project) do - create(:project, :public) do |project| + create(:project, :public, :repository) do |project| create(:project_member, user: contributor, project: project) end end @@ -40,13 +40,13 @@ describe Gitlab::ContributionsCalendar do described_class.new(contributor, current_user) end - def create_event(project, day, hour = 0) + def create_event(project, day, hour = 0, action = Event::CREATED, target_symbol = :issue) @targets ||= {} - @targets[project] ||= create(:issue, project: project, author: contributor) + @targets[project] ||= create(target_symbol, project: project, author: contributor) Event.create!( project: project, - action: Event::CREATED, + action: action, target: @targets[project], author: contributor, created_at: DateTime.new(day.year, day.month, day.day, hour) @@ -71,6 +71,12 @@ describe Gitlab::ContributionsCalendar do expect(calendar(contributor).activity_dates[today]).to eq(2) end + it "counts the diff notes on merge request" do + create_event(public_project, today, 0, Event::COMMENTED, :diff_note_on_merge_request) + + expect(calendar(contributor).activity_dates[today]).to eq(1) + end + context "when events fall under different dates depending on the time zone" do before do create_event(public_project, today, 1) diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 91c43f2bdc0..ee91decafad 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_status]).to eq(build.status) } it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } - it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } + it { expect(data[:project_name]).to eq(build.project.full_name) } context 'commit author_url' do context 'when no commit present' do diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 031efcf1291..53899e00b53 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -55,8 +55,8 @@ describe Gitlab::Email::Handler::CreateNoteHandler do expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) end - context 'because the note was commands only' do - let!(:email_raw) { fixture_file("emails/commands_only_reply.eml") } + context 'because the note was update commands only' do + let!(:email_raw) { fixture_file("emails/update_commands_only_reply.eml") } context 'and current user cannot update noteable' do it 'raises a CommandsOnlyNoteError' do @@ -70,13 +70,10 @@ describe Gitlab::Email::Handler::CreateNoteHandler do end it 'does not raise an error' do - expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy - # One system note is created for the 'close' event expect { receiver.execute }.to change { noteable.notes.count }.by(1) expect(noteable.reload).to be_closed - expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end end @@ -85,15 +82,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler do context 'when the note contains quick actions' do let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") } - context 'and current user cannot update noteable' do - it 'post a note and does not update the noteable' do - expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy - - # One system note is created for the new note - expect { receiver.execute }.to change { noteable.notes.count }.by(1) + context 'and current user cannot update the noteable' do + it 'only executes the commands that the user can perform' do + expect { receiver.execute } + .to change { noteable.notes.user.count }.by(1) + .and change { user.todos_pending_count }.from(0).to(1) expect(noteable.reload).to be_open - expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy end end @@ -102,14 +97,14 @@ describe Gitlab::Email::Handler::CreateNoteHandler do project.add_developer(user) end - it 'post a note and updates the noteable' do + it 'posts a note and updates the noteable' do expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy - # One system note is created for the new note, one for the 'close' event - expect { receiver.execute }.to change { noteable.notes.count }.by(2) + expect { receiver.execute } + .to change { noteable.notes.user.count }.by(1) + .and change { user.todos_pending_count }.from(0).to(1) expect(noteable.reload).to be_closed - expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index a6341cd509b..67d898e787e 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -500,4 +500,33 @@ describe Gitlab::Git::Blob, seed_helper: true do end end end + + describe '#load_all_data!' do + let(:full_data) { 'abcd' } + let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: 'abc') } + + subject { blob.load_all_data!(repository) } + + it 'loads missing data' do + expect(Gitlab::GitalyClient).to receive(:migrate) + .with(:git_blob_load_all_data).and_return(full_data) + + subject + + expect(blob.data).to eq(full_data) + end + + context 'with a fully loaded blob' do + let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: full_data) } + + it "doesn't perform any loading" do + expect(Gitlab::GitalyClient).not_to receive(:migrate) + .with(:git_blob_load_all_data) + + subject + + expect(blob.data).to eq(full_data) + end + end + end end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 708870060e7..a19155ed5b0 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -59,5 +59,69 @@ describe Gitlab::Git::Branch, seed_helper: true do it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) } end + context 'with active, stale and future branches' do + let(:repository) do + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + end + + let(:user) { create(:user) } + let(:committer) do + Gitlab::Git.committer_hash(email: user.email, name: user.name) + end + let(:params) do + parents = [repository.rugged.head.target] + tree = parents.first.tree + + { + message: 'commit message', + author: committer, + committer: committer, + tree: tree, + parents: parents + } + end + let(:stale_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } } + let(:active_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } } + let(:future_sha) { Timecop.freeze(100.days.since) { create_commit } } + + before do + repository.create_branch('stale-1', stale_sha) + repository.create_branch('active-1', active_sha) + repository.create_branch('future-1', future_sha) + end + + after do + ensure_seeds + end + + describe 'examine if the branch is active or stale' do + let(:stale_branch) { repository.find_branch('stale-1') } + let(:active_branch) { repository.find_branch('active-1') } + let(:future_branch) { repository.find_branch('future-1') } + + describe '#active?' do + it { expect(stale_branch.active?).to be_falsey } + it { expect(active_branch.active?).to be_truthy } + it { expect(future_branch.active?).to be_truthy } + end + + describe '#stale?' do + it { expect(stale_branch.stale?).to be_truthy } + it { expect(active_branch.stale?).to be_falsey } + it { expect(future_branch.stale?).to be_falsey } + end + + describe '#state' do + it { expect(stale_branch.state).to eq(:stale) } + it { expect(active_branch.state).to eq(:active) } + it { expect(future_branch.state).to eq(:active) } + end + end + end + 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))) + end end diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb index c9007d7d456..d0dd8c6303f 100644 --- a/spec/lib/gitlab/git/lfs_changes_spec.rb +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -7,34 +7,36 @@ describe Gitlab::Git::LfsChanges do subject { described_class.new(project.repository, newrev) } - describe 'new_pointers' do - before do - allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_yield([blob_object_id]) + describe '#new_pointers' do + shared_examples 'new pointers' do + it 'filters new objects to find lfs pointers' do + expect(subject.new_pointers(not_in: []).first.id).to eq(blob_object_id) + end + + it 'limits new_objects using object_limit' do + expect(subject.new_pointers(object_limit: 1)).to eq([]) + end end - it 'uses rev-list to find new objects' do - rev_list = double - allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) - - expect(rev_list).to receive(:new_objects).and_return([]) - - subject.new_pointers + context 'with gitaly enabled' do + it_behaves_like 'new pointers' end - it 'filters new objects to find lfs pointers' do - expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id]) + context 'with gitaly disabled', :skip_gitaly_mock do + it_behaves_like 'new pointers' - subject.new_pointers(object_limit: 1) - end + it 'uses rev-list to find new objects' do + rev_list = double + allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) - it 'limits new_objects using object_limit' do - expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, []) + expect(rev_list).to receive(:new_objects).and_return([]) - subject.new_pointers(object_limit: 0) + subject.new_pointers + end end end - describe 'all_pointers' do + describe '#all_pointers', :skip_gitaly_mock do it 'uses rev-list to find all objects' do rev_list = double allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 25defb98b7c..52c9876cbb6 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -751,255 +751,263 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#log" do - let(:commit_with_old_name) do - Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) - end - let(:commit_with_new_name) do - Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id) - end - let(:rename_commit) do - Gitlab::Git::Commit.decorate(repository, @rename_commit_id) - end - - before(:context) do - # Add new commits so that there's a renamed file in the commit history - repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged - @commit_with_old_name_id = new_commit_edit_old_file(repo) - @rename_commit_id = new_commit_move_file(repo) - @commit_with_new_name_id = new_commit_edit_new_file(repo) - end - - after(:context) do - # Erase our commits so other tests get the original repo - repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged - repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) - end - - context "where 'follow' == true" do - let(:options) { { ref: "master", follow: true } } + shared_examples 'repository log' do + let(:commit_with_old_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) + end + let(:commit_with_new_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id) + end + let(:rename_commit) do + Gitlab::Git::Commit.decorate(repository, @rename_commit_id) + end - context "and 'path' is a directory" do - it "does not follow renames" do - log_commits = repository.log(options.merge(path: "encoding")) + before(:context) do + # Add new commits so that there's a renamed file in the commit history + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + @commit_with_old_name_id = new_commit_edit_old_file(repo) + @rename_commit_id = new_commit_move_file(repo) + @commit_with_new_name_id = new_commit_edit_new_file(repo) + end - aggregate_failures do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) - end - end + after(:context) do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) end - context "and 'path' is a file that matches the new filename" do - context 'without offset' do - it "follows renames" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG")) + context "where 'follow' == true" do + let(:options) { { ref: "master", follow: true } } + + context "and 'path' is a directory" do + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "encoding")) aggregate_failures do expect(log_commits).to include(commit_with_new_name) expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + expect(log_commits).not_to include(commit_with_old_name) end end end - context 'with offset=1' do - it "follows renames and skip the latest commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1)) + context "and 'path' is a file that matches the new filename" do + context 'without offset' do + it "follows renames" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG")) - aggregate_failures do - expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + aggregate_failures do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end end end - end - context 'with offset=1', 'and limit=1' do - it "follows renames, skip the latest commit and return only one commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1)) + context 'with offset=1' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1)) - expect(log_commits).to contain_exactly(rename_commit) + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end - end - context 'with offset=1', 'and limit=2' do - it "follows renames, skip the latest commit and return only two commits" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2)) + context 'with offset=1', 'and limit=1' do + it "follows renames, skip the latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1)) - aggregate_failures do - expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name) + expect(log_commits).to contain_exactly(rename_commit) end end - end - context 'with offset=2' do - it "follows renames and skip the latest commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2)) + context 'with offset=1', 'and limit=2' do + it "follows renames, skip the latest commit and return only two commits" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2)) - aggregate_failures do - expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).not_to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + aggregate_failures do + expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name) + end end end - end - context 'with offset=2', 'and limit=1' do - it "follows renames, skip the two latest commit and return only one commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1)) + context 'with offset=2' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2)) - expect(log_commits).to contain_exactly(commit_with_old_name) + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end + end + + context 'with offset=2', 'and limit=1' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1)) + + expect(log_commits).to contain_exactly(commit_with_old_name) + end + end + + context 'with offset=2', 'and limit=2' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end end - context 'with offset=2', 'and limit=2' do - it "follows renames, skip the two latest commit and return only one commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2)) + context "and 'path' is a file that matches the old filename" do + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "CHANGELOG")) aggregate_failures do expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(rename_commit) expect(log_commits).to include(commit_with_old_name) end end end - end - context "and 'path' is a file that matches the old filename" do - it "does not follow renames" do - log_commits = repository.log(options.merge(path: "CHANGELOG")) + context "unknown ref" do + it "returns an empty array" do + log_commits = repository.log(options.merge(ref: 'unknown')) - aggregate_failures do - expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to eq([]) end end end - context "unknown ref" do - it "returns an empty array" do - log_commits = repository.log(options.merge(ref: 'unknown')) - - expect(log_commits).to eq([]) - end - end - end + context "where 'follow' == false" do + options = { follow: false } - context "where 'follow' == false" do - options = { follow: false } + context "and 'path' is a directory" do + let(:log_commits) do + repository.log(options.merge(path: "encoding")) + end - context "and 'path' is a directory" do - let(:log_commits) do - repository.log(options.merge(path: "encoding")) + it "does not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end end - it "does not follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) - end - end + context "and 'path' is a file that matches the new filename" do + let(:log_commits) do + repository.log(options.merge(path: "encoding/CHANGELOG")) + end - context "and 'path' is a file that matches the new filename" do - let(:log_commits) do - repository.log(options.merge(path: "encoding/CHANGELOG")) + it "does not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end end - it "does not follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) - end - end + context "and 'path' is a file that matches the old filename" do + let(:log_commits) do + repository.log(options.merge(path: "CHANGELOG")) + end - context "and 'path' is a file that matches the old filename" do - let(:log_commits) do - repository.log(options.merge(path: "CHANGELOG")) + it "does not follow renames" do + expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_new_name) + end end - it "does not follow renames" do - expect(log_commits).to include(commit_with_old_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_new_name) + context "and 'path' includes a directory that used to be a file" do + let(:log_commits) do + repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) + end + + it "returns a list of commits" do + expect(log_commits.size).to eq(1) + end end end - context "and 'path' includes a directory that used to be a file" do - let(:log_commits) do - repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) - end + context "where provides 'after' timestamp" do + options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "returns a list of commits" do - expect(log_commits.size).to eq(1) + it "should returns commits on or after that timestamp" do + commits = repository.log(options) + + expect(commits.size).to be > 0 + expect(commits).to satisfy do |commits| + commits.all? { |commit| commit.committed_date >= options[:after] } + end end end - end - context "where provides 'after' timestamp" do - options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } + context "where provides 'before' timestamp" do + options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "should returns commits on or after that timestamp" do - commits = repository.log(options) + it "should returns commits on or before that timestamp" do + commits = repository.log(options) - expect(commits.size).to be > 0 - expect(commits).to satisfy do |commits| - commits.all? { |commit| commit.committed_date >= options[:after] } + expect(commits.size).to be > 0 + expect(commits).to satisfy do |commits| + commits.all? { |commit| commit.committed_date <= options[:before] } + end end end - end - context "where provides 'before' timestamp" do - options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } + context 'when multiple paths are provided' do + let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } - it "should returns commits on or before that timestamp" do - commits = repository.log(options) - - expect(commits.size).to be > 0 - expect(commits).to satisfy do |commits| - commits.all? { |commit| commit.committed_date <= options[:before] } + def commit_files(commit) + commit.rugged_diff_from_parent.deltas.flat_map do |delta| + [delta.old_file[:path], delta.new_file[:path]].uniq.compact + end end - end - end - context 'when multiple paths are provided' do - let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } + it 'only returns commits matching at least one path' do + commits = repository.log(options) - def commit_files(commit) - commit.rugged_diff_from_parent.deltas.flat_map do |delta| - [delta.old_file[:path], delta.new_file[:path]].uniq.compact + expect(commits.size).to be > 0 + expect(commits).to satisfy do |commits| + commits.none? { |commit| (commit_files(commit) & options[:path]).empty? } + end end end - it 'only returns commits matching at least one path' do - commits = repository.log(options) + context 'limit validation' do + where(:limit) do + [0, nil, '', 'foo'] + end - expect(commits.size).to be > 0 - expect(commits).to satisfy do |commits| - commits.none? { |commit| (commit_files(commit) & options[:path]).empty? } + with_them do + it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } end end - end - context 'limit validation' do - where(:limit) do - [0, nil, '', 'foo'] - end + context 'with all' do + it 'returns a list of commits' do + commits = repository.log({ all: true, limit: 50 }) - with_them do - it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } + expect(commits.size).to eq(37) + end end end - context 'with all' do - let(:options) { { all: true, limit: 50 } } - - it 'returns a list of commits' do - commits = repository.log(options) + context 'when Gitaly find_commits feature is enabled' do + it_behaves_like 'repository log' + end - expect(commits.size).to eq(37) - end + context 'when Gitaly find_commits feature is disabled', :disable_gitaly do + it_behaves_like 'repository log' end end @@ -1136,14 +1144,6 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(repository.count_commits(options)).to eq(10) end end - end - - context 'when Gitaly count_commits feature is enabled' do - it_behaves_like 'extended commit counting' - end - - context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do - it_behaves_like 'extended commit counting' context "with all" do it "returns the number of commits in the whole repository" do @@ -1155,10 +1155,18 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'without all or ref being specified' do it "raises an ArgumentError" do - expect { repository.count_commits({}) }.to raise_error(ArgumentError, "Please specify a valid ref or set the 'all' attribute to true") + expect { repository.count_commits({}) }.to raise_error(ArgumentError) end end end + + context 'when Gitaly count_commits feature is enabled' do + it_behaves_like 'extended commit counting' + end + + context 'when Gitaly count_commits feature is disabled', :disable_gitaly do + it_behaves_like 'extended commit counting' + end end describe '#autocrlf' do diff --git a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb new file mode 100644 index 00000000000..a2770ef2fe4 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::BlobService do + let(:project) { create(:project, :repository) } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.disk_path + '.git' } + let(:repository) { project.repository } + let(:client) { described_class.new(repository) } + + describe '#get_new_lfs_pointers' do + let(:revision) { 'master' } + let(:limit) { 5 } + let(:not_in) { ['branch-a', 'branch-b'] } + let(:expected_params) do + { revision: revision, limit: limit, not_in_refs: not_in, not_in_all: false } + end + + subject { client.get_new_lfs_pointers(revision, limit, not_in) } + + it 'sends a get_new_lfs_pointers message' do + expect_any_instance_of(Gitaly::BlobService::Stub) + .to receive(:get_new_lfs_pointers) + .with(gitaly_request_with_params(expected_params), kind_of(Hash)) + .and_return([]) + + subject + end + + context 'with not_in = :all' do + let(:not_in) { :all } + let(:expected_params) do + { revision: revision, limit: limit, not_in_refs: [], not_in_all: true } + end + + it 'sends the correct message' do + expect_any_instance_of(Gitaly::BlobService::Stub) + .to receive(:get_new_lfs_pointers) + .with(gitaly_request_with_params(expected_params), kind_of(Hash)) + .and_return([]) + + subject + end + end + end + + describe '#get_all_lfs_pointers' do + let(:revision) { 'master' } + + subject { client.get_all_lfs_pointers(revision) } + + it 'sends a get_all_lfs_pointers message' do + expect_any_instance_of(Gitaly::BlobService::Stub) + .to receive(:get_all_lfs_pointers) + .with(gitaly_request_with_params(revision: revision), kind_of(Hash)) + .and_return([]) + + subject + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 41a55027f4d..b20cc34dd5c 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -277,6 +277,7 @@ project: - fork_network - custom_attributes - lfs_file_locks +- project_badges award_emoji: - awardable - user @@ -293,3 +294,5 @@ issue_assignees: - assignee lfs_file_locks: - user +project_badges: +- project diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb index a93a921e459..4897d604bc1 100644 --- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarRestorer do include UploadHelpers - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } + let(:shared) { project.import_export_shared } let(:project) { create(:project) } before do diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb index 3fb5ddde8b5..f40d4bc2d08 100644 --- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarSaver do - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } + let(:shared) { project.import_export_shared } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:project_with_avatar) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } let(:project) { create(:project) } diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 5cdc5138fda..58b9fb06cc5 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::ImportExport::FileImporter do - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } + let(:shared) { Gitlab::ImportExport::Shared.new(nil) } let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" } let(:valid_file) { "#{shared.export_path}/valid.json" } let(:symlink_file) { "#{shared.export_path}/invalid.json" } @@ -12,6 +12,7 @@ describe Gitlab::ImportExport::FileImporter do stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) + allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:relative_archive_path).and_return('test') allow(SecureRandom).to receive(:hex).and_return('abcd') setup_files end diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index cfb15ee7e8b..17e06a6a83f 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -7,7 +7,7 @@ describe 'forked project import' do let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } let(:forked_from_project) { create(:project, :repository) } let(:forked_project) { fork_project(project_with_repo, nil, repository: true) } let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index b6c1f0c81cb..62ef93f847a 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -14,8 +14,7 @@ "template": false, "description": "", "type": "ProjectLabel", - "priorities": [ - ] + "priorities": [] }, { "id": 3, @@ -160,9 +159,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 352, @@ -184,9 +181,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 353, @@ -208,9 +203,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 354, @@ -232,9 +225,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 355, @@ -256,9 +247,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 356, @@ -280,9 +269,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 357, @@ -304,9 +291,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 358, @@ -328,9 +313,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -395,9 +378,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 360, @@ -419,9 +400,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 361, @@ -443,9 +422,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 362, @@ -467,9 +444,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 363, @@ -491,9 +466,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 364, @@ -515,9 +488,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 365, @@ -539,9 +510,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 366, @@ -563,9 +532,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -628,9 +595,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 368, @@ -652,9 +617,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 369, @@ -676,9 +639,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 370, @@ -700,9 +661,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 371, @@ -724,9 +683,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 372, @@ -748,9 +705,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 373, @@ -772,9 +727,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 374, @@ -796,9 +749,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -840,9 +791,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 376, @@ -864,9 +813,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 377, @@ -888,9 +835,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 378, @@ -912,9 +857,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 379, @@ -936,9 +879,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 380, @@ -960,9 +901,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 381, @@ -984,9 +923,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 382, @@ -1008,9 +945,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1052,9 +987,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 384, @@ -1076,9 +1009,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 385, @@ -1100,9 +1031,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 386, @@ -1124,9 +1053,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 387, @@ -1148,9 +1075,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 388, @@ -1172,9 +1097,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 389, @@ -1196,9 +1119,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 390, @@ -1220,9 +1141,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1264,9 +1183,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 392, @@ -1288,9 +1205,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 393, @@ -1312,9 +1227,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 394, @@ -1336,9 +1249,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 395, @@ -1360,9 +1271,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 396, @@ -1384,9 +1293,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 397, @@ -1408,9 +1315,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 398, @@ -1432,9 +1337,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1476,9 +1379,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 400, @@ -1500,9 +1401,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 401, @@ -1524,9 +1423,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 402, @@ -1548,9 +1445,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 403, @@ -1572,9 +1467,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 404, @@ -1596,9 +1489,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 405, @@ -1620,9 +1511,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 406, @@ -1644,9 +1533,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1688,9 +1575,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 408, @@ -1712,9 +1597,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 409, @@ -1736,9 +1619,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 410, @@ -1760,9 +1641,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 411, @@ -1784,9 +1663,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 412, @@ -1808,9 +1685,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 413, @@ -1832,9 +1707,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 414, @@ -1856,9 +1729,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1900,9 +1771,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 416, @@ -1924,9 +1793,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 417, @@ -1948,9 +1815,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 418, @@ -1972,9 +1837,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 419, @@ -1996,9 +1859,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 420, @@ -2020,9 +1881,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 421, @@ -2044,9 +1903,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 422, @@ -2068,9 +1925,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -2112,9 +1967,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 424, @@ -2136,9 +1989,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 425, @@ -2160,9 +2011,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 426, @@ -2184,9 +2033,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 427, @@ -2208,9 +2055,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 428, @@ -2232,9 +2077,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 429, @@ -2256,9 +2099,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 430, @@ -2280,9 +2121,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] } @@ -2378,12 +2217,8 @@ ] } ], - "snippets": [ - - ], - "releases": [ - - ], + "snippets": [], + "releases": [], "project_members": [ { "id": 36, @@ -2515,9 +2350,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 672, @@ -2539,9 +2372,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 673, @@ -2563,9 +2394,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 674, @@ -2587,9 +2416,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 675, @@ -2611,9 +2438,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 676, @@ -2635,9 +2460,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 677, @@ -2659,9 +2482,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 678, @@ -2683,9 +2504,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -2696,7 +2515,7 @@ "merge_request_diff_id": 27, "relative_order": 0, "sha": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc", - "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-08-06T08:35:52.000+02:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2708,7 +2527,7 @@ "merge_request_diff_id": 27, "relative_order": 1, "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T10:01:38.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2720,7 +2539,7 @@ "merge_request_diff_id": 27, "relative_order": 2, "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:57:31.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2732,7 +2551,7 @@ "merge_request_diff_id": 27, "relative_order": 3, "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:54:21.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2744,7 +2563,7 @@ "merge_request_diff_id": 27, "relative_order": 4, "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:49:50.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2756,7 +2575,7 @@ "merge_request_diff_id": 27, "relative_order": 5, "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:48:32.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2834,7 +2653,7 @@ { "merge_request_diff_id": 27, "relative_order": 5, - "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -2958,9 +2777,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 680, @@ -2982,9 +2799,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 681, @@ -3006,9 +2821,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 682, @@ -3030,9 +2843,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 683, @@ -3054,9 +2865,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 684, @@ -3078,9 +2887,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 685, @@ -3102,9 +2909,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 686, @@ -3126,9 +2931,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -3139,7 +2942,7 @@ "merge_request_diff_id": 26, "sha": "0b4bc9a49b562e85de7cc9e834518ea6828729b9", "relative_order": 0, - "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:26:01.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3237,9 +3040,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 778, @@ -3261,9 +3062,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 779, @@ -3285,9 +3084,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 780, @@ -3309,9 +3106,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 781, @@ -3333,9 +3128,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 782, @@ -3357,9 +3150,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 783, @@ -3381,9 +3172,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 784, @@ -3405,9 +3194,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -3516,9 +3303,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 786, @@ -3540,9 +3325,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 787, @@ -3564,9 +3347,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 788, @@ -3588,9 +3369,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 789, @@ -3612,9 +3391,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 790, @@ -3636,9 +3413,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 791, @@ -3660,9 +3435,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 792, @@ -3684,9 +3457,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -3877,7 +3648,7 @@ "merge_request_diff_id": 14, "relative_order": 15, "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T10:01:38.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3889,7 +3660,7 @@ "merge_request_diff_id": 14, "relative_order": 16, "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:57:31.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3901,7 +3672,7 @@ "merge_request_diff_id": 14, "relative_order": 17, "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:54:21.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3913,7 +3684,7 @@ "merge_request_diff_id": 14, "relative_order": 18, "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:49:50.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3925,7 +3696,7 @@ "merge_request_diff_id": 14, "relative_order": 19, "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:48:32.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -4016,7 +3787,7 @@ { "merge_request_diff_id": 14, "relative_order": 6, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -4042,7 +3813,7 @@ { "merge_request_diff_id": 14, "relative_order": 8, - "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -4207,7 +3978,7 @@ }, "events": [ { - "merge_request_diff_id": 14, + "merge_request_diff_id": 14, "id": 529, "target_type": "Note", "target_id": 793, @@ -4239,9 +4010,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 795, @@ -4263,9 +4032,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 796, @@ -4287,9 +4054,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 797, @@ -4311,9 +4076,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 798, @@ -4335,9 +4098,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 799, @@ -4359,9 +4120,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 800, @@ -4383,9 +4142,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -4603,7 +4360,7 @@ { "merge_request_diff_id": 13, "relative_order": 2, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -4740,9 +4497,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 802, @@ -4764,9 +4519,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 803, @@ -4788,9 +4541,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 804, @@ -4812,9 +4563,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 805, @@ -4836,9 +4585,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 806, @@ -4860,9 +4607,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 807, @@ -4884,9 +4629,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 808, @@ -4908,9 +4651,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -5104,7 +4845,7 @@ { "merge_request_diff_id": 12, "relative_order": 2, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -5228,9 +4969,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 810, @@ -5252,9 +4991,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 811, @@ -5276,9 +5013,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 812, @@ -5300,9 +5035,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 813, @@ -5324,9 +5057,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 814, @@ -5348,9 +5079,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 815, @@ -5372,9 +5101,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 816, @@ -5396,18 +5123,14 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { "id": 11, "state": "empty", - "merge_request_diff_commits": [ - ], - "merge_request_diff_files": [ - ], + "merge_request_diff_commits": [], + "merge_request_diff_files": [], "merge_request_id": 11, "created_at": "2016-06-14T15:02:23.772Z", "updated_at": "2016-06-14T15:02:23.833Z", @@ -5482,9 +5205,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 818, @@ -5506,9 +5227,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 819, @@ -5530,9 +5249,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 820, @@ -5554,9 +5271,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 821, @@ -5578,9 +5293,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 822, @@ -5602,9 +5315,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 823, @@ -5626,9 +5337,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 824, @@ -5650,9 +5359,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -5843,7 +5550,7 @@ "merge_request_diff_id": 10, "relative_order": 16, "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T10:01:38.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5855,7 +5562,7 @@ "merge_request_diff_id": 10, "relative_order": 17, "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:57:31.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5867,7 +5574,7 @@ "merge_request_diff_id": 10, "relative_order": 18, "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:54:21.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5879,7 +5586,7 @@ "merge_request_diff_id": 10, "relative_order": 19, "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:49:50.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5891,7 +5598,7 @@ "merge_request_diff_id": 10, "relative_order": 20, "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:48:32.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5982,7 +5689,7 @@ { "merge_request_diff_id": 10, "relative_order": 6, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -6008,7 +5715,7 @@ { "merge_request_diff_id": 10, "relative_order": 8, - "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -6171,9 +5878,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 826, @@ -6195,9 +5900,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 827, @@ -6219,9 +5922,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 828, @@ -6243,9 +5944,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 829, @@ -6267,9 +5966,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 830, @@ -6291,9 +5988,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 831, @@ -6315,9 +6010,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 832, @@ -6339,9 +6032,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -6953,9 +6644,7 @@ "updated_at": "2017-01-16T15:25:28.637Z" } ], - "deploy_keys": [ - - ], + "deploy_keys": [], "services": [ { "id": 100, @@ -6964,9 +6653,7 @@ "created_at": "2016-06-14T15:01:51.315Z", "updated_at": "2016-06-14T15:01:51.315Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7008,9 +6695,7 @@ "created_at": "2016-06-14T15:01:51.289Z", "updated_at": "2016-06-14T15:01:51.289Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7030,9 +6715,7 @@ "created_at": "2016-06-14T15:01:51.277Z", "updated_at": "2016-06-14T15:01:51.277Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7052,9 +6735,7 @@ "created_at": "2016-06-14T15:01:51.267Z", "updated_at": "2016-06-14T15:01:51.267Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7097,9 +6778,7 @@ "created_at": "2016-06-14T15:01:51.232Z", "updated_at": "2016-06-14T15:01:51.232Z", "active": true, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7141,9 +6820,7 @@ "created_at": "2016-06-14T15:01:51.202Z", "updated_at": "2016-06-14T15:01:51.202Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7163,9 +6840,7 @@ "created_at": "2016-06-14T15:01:51.182Z", "updated_at": "2016-06-14T15:01:51.182Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7185,9 +6860,7 @@ "created_at": "2016-06-14T15:01:51.166Z", "updated_at": "2016-06-14T15:01:51.166Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7207,9 +6880,7 @@ "created_at": "2016-06-14T15:01:51.153Z", "updated_at": "2016-06-14T15:01:51.153Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7229,9 +6900,7 @@ "created_at": "2016-06-14T15:01:51.139Z", "updated_at": "2016-06-14T15:01:51.139Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7251,9 +6920,7 @@ "created_at": "2016-06-14T15:01:51.125Z", "updated_at": "2016-06-14T15:01:51.125Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7273,9 +6940,7 @@ "created_at": "2016-06-14T15:01:51.113Z", "updated_at": "2016-06-14T15:01:51.113Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7295,9 +6960,7 @@ "created_at": "2016-06-14T15:01:51.080Z", "updated_at": "2016-06-14T15:01:51.080Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7317,9 +6980,7 @@ "created_at": "2016-06-14T15:01:51.067Z", "updated_at": "2016-06-14T15:01:51.067Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7339,9 +7000,7 @@ "created_at": "2016-06-14T15:01:51.047Z", "updated_at": "2016-06-14T15:01:51.047Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7361,9 +7020,7 @@ "created_at": "2016-06-14T15:01:51.031Z", "updated_at": "2016-06-14T15:01:51.031Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7383,9 +7040,7 @@ "created_at": "2016-06-14T15:01:51.031Z", "updated_at": "2016-06-14T15:01:51.031Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7399,9 +7054,7 @@ "type": "JenkinsDeprecatedService" } ], - "hooks": [ - - ], + "hooks": [], "protected_branches": [ { "id": 1, @@ -7475,5 +7128,25 @@ "key": "bar", "value": "bar" } + ], + "project_badges": [ + { + "id": 1, + "created_at": "2017-10-19T15:36:23.466Z", + "updated_at": "2017-10-19T15:36:23.466Z", + "project_id": 5, + "type": "ProjectBadge", + "link_url": "http://www.example.com", + "image_url": "http://www.example.com" + }, + { + "id": 2, + "created_at": "2017-10-19T15:36:23.466Z", + "updated_at": "2017-10-19T15:36:23.466Z", + "project_id": 5, + "type": "ProjectBadge", + "link_url": "http://www.example.com", + "image_url": "http://www.example.com" + } ] } diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index d076007e4bc..f4e466d1296 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -7,9 +7,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do @user = create(:user) RSpec::Mocks.with_temporary_scope do - @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') - allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') @project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') + @shared = @project.import_export_shared + allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') allow_any_instance_of(Repository).to receive(:fetch_ref).and_return(true) allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) @@ -129,6 +129,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(@project.custom_attributes.count).to eq(2) end + it 'has badges' do + expect(@project.project_badges.count).to eq(2) + end + it 'restores the correct service' do expect(CustomIssueTrackerService.first).not_to be_nil end @@ -259,7 +263,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do context 'Light JSON' do let(:user) { create(:user) } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } + let(:shared) { project.import_export_shared } let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restored_project_json) { project_tree_restorer.restore } diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 5804c45871e..3049491f0ae 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::ImportExport::ProjectTreeSaver do describe 'saves the project tree into a json object' do - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:user) { create(:user) } @@ -180,6 +180,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['custom_attributes'].count).to eq(2) end + it 'has badges' do + expect(saved_project_json['project_badges'].count).to eq(2) + end + it 'does not complain about non UTF-8 characters in MR diff files' do ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") @@ -288,6 +292,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:project_custom_attribute, project: project) create(:project_custom_attribute, project: project) + create(:project_badge, project: project) + create(:project_badge, project: project) + project end diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index e9f5273725d..1ef024d3078 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::ImportExport::Reader do - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } + let(:shared) { Gitlab::ImportExport::Shared.new(nil) } let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:project_tree_hash) do { diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index c49af602a01..dc806d036ff 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::ImportExport::RepoRestorer do let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(:project) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } let(:restorer) do diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb index 44f972fe530..187ec8fcfa2 100644 --- a/spec/lib/gitlab/import_export/repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ImportExport::RepoSaver do let(:user) { create(:user) } let!(:project) { create(:project, :public, name: 'searchable_project') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } let(:bundler) { described_class.new(project: project, shared: shared) } before do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index feaab6673cd..ddcbb7a0033 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -536,3 +536,12 @@ LfsFileLock: - user_id - project_id - created_at +Badge: +- id +- link_url +- image_url +- project_id +- group_id +- created_at +- updated_at +- type diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb index 8a3a244be21..acef97459b8 100644 --- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::UploadsRestorer do describe 'bundle a project Git repo' do let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index 177036c109b..1304d8fabfc 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::ImportExport::UploadsSaver do describe 'bundle a project Git repo' do let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" } let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb index e7d50f75682..49d857d9483 100644 --- a/spec/lib/gitlab/import_export/version_checker_spec.rb +++ b/spec/lib/gitlab/import_export/version_checker_spec.rb @@ -2,12 +2,13 @@ require 'spec_helper' include ImportExport::CommonUtil describe Gitlab::ImportExport::VersionChecker do - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } + let(:shared) { Gitlab::ImportExport::Shared.new(nil) } describe 'bundle a project Git repo' do let(:version) { Gitlab::ImportExport.version } before do + allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:relative_archive_path).and_return('') allow(File).to receive(:open).and_return(version) end diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb index 1d1e7e7f89a..d2bd8ccdf3f 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ImportExport::WikiRepoSaver do let(:user) { create(:user) } let!(:project) { create(:project, :public, name: 'searchable_project') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } let(:wiki_bundler) { described_class.new(project: project, shared: shared) } let!(:project_wiki) { ProjectWiki.new(project, user) } diff --git a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb index 81b654e9c5f..5c01ee0ebb8 100644 --- a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::ImportExport::WikiRestorer do let!(:project_without_wiki) { create(:project) } let!(:project) { create(:project) } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } + let(:shared) { project.import_export_shared } let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } let(:restorer) do diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index 07ba11b93a3..39ec2f37a83 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -11,15 +11,17 @@ describe Gitlab::Middleware::ReadOnly do RSpec::Matchers.define :disallow_request do match do |middleware| - flash = middleware.send(:rack_flash) - flash['alert'] && flash['alert'].include?('You cannot do writing operations') + alert = middleware.env['rack.session'].to_hash + .dig('flash', 'flashes', 'alert') + + alert&.include?('You cannot perform write operations') end end RSpec::Matchers.define :disallow_request_in_json do match do |response| json_response = JSON.parse(response.body) - response.body.include?('You cannot do writing operations') && json_response.key?('message') + response.body.include?('You cannot perform write operations') && json_response.key?('message') end end @@ -34,10 +36,25 @@ describe Gitlab::Middleware::ReadOnly do rack.to_app end - subject { described_class.new(fake_app) } + let(:observe_env) do + Module.new do + attr_reader :env + + def call(env) + @env = env + super + end + end + end let(:request) { Rack::MockRequest.new(rack_stack) } + subject do + described_class.new(fake_app).tap do |app| + app.extend(observe_env) + end + end + context 'normal requests to a read-only Gitlab instance' do let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } diff --git a/spec/lib/gitlab/middleware/release_env_spec.rb b/spec/lib/gitlab/middleware/release_env_spec.rb new file mode 100644 index 00000000000..5e3aa877409 --- /dev/null +++ b/spec/lib/gitlab/middleware/release_env_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::Middleware::ReleaseEnv do + let(:inner_app) { double(:app, call: 'yay') } + let(:app) { described_class.new(inner_app) } + let(:env) { { 'action_controller.instance' => 'something' } } + + describe '#call' do + it 'calls the app and clears the env' do + result = app.call(env) + + expect(result).to eq('yay') + expect(env).to be_empty + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index d8250e4b4c6..c46bb8edebf 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -217,7 +217,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).not_to include security_issue_1 expect(issues).not_to include security_issue_2 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'does not list project confidential issues for project members with guest role' do @@ -229,7 +229,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).not_to include security_issue_1 expect(issues).not_to include security_issue_2 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'lists project confidential issues for author' do @@ -239,7 +239,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).to include security_issue_1 expect(issues).not_to include security_issue_2 - expect(results.issues_count).to eq 2 + expect(results.limited_issues_count).to eq 2 end it 'lists project confidential issues for assignee' do @@ -249,7 +249,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).not_to include security_issue_1 expect(issues).to include security_issue_2 - expect(results.issues_count).to eq 2 + expect(results.limited_issues_count).to eq 2 end it 'lists project confidential issues for project members' do @@ -261,7 +261,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).to include security_issue_1 expect(issues).to include security_issue_2 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end it 'lists all project issues for admin' do @@ -271,7 +271,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).to include security_issue_1 expect(issues).to include security_issue_2 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end end @@ -304,6 +304,35 @@ describe Gitlab::ProjectSearchResults do end end + describe '#limited_notes_count' do + let(:project) { create(:project, :public) } + let(:note) { create(:note_on_issue, project: project) } + let(:results) { described_class.new(user, project, note.note) } + + context 'when count_limit is lower than total amount' do + before do + allow(results).to receive(:count_limit).and_return(1) + end + + it 'calls note finder once to get the limited amount of notes' do + expect(results).to receive(:notes_finder).once.and_call_original + expect(results.limited_notes_count).to eq(1) + end + end + + context 'when count_limit is higher than total amount' do + it 'calls note finder multiple times to get the limited amount of notes' do + project = create(:project, :public) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results).to receive(:notes_finder).exactly(4).times.and_call_original + expect(results.limited_notes_count).to eq(1) + end + end + end + # Examples for commit access level test # # params: diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 9dbab95f70e..87288baedb0 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -29,30 +29,6 @@ describe Gitlab::SearchResults do end end - describe '#projects_count' do - it 'returns the total amount of projects' do - expect(results.projects_count).to eq(1) - end - end - - describe '#issues_count' do - it 'returns the total amount of issues' do - expect(results.issues_count).to eq(1) - end - end - - describe '#merge_requests_count' do - it 'returns the total amount of merge requests' do - expect(results.merge_requests_count).to eq(1) - end - end - - describe '#milestones_count' do - it 'returns the total amount of milestones' do - expect(results.milestones_count).to eq(1) - end - end - context "when count_limit is lower than total amount" do before do allow(results).to receive(:count_limit).and_return(1) @@ -183,7 +159,7 @@ describe Gitlab::SearchResults do expect(issues).not_to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'does not list confidential issues for project members with guest role' do @@ -199,7 +175,7 @@ describe Gitlab::SearchResults do expect(issues).not_to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'lists confidential issues for author' do @@ -212,7 +188,7 @@ describe Gitlab::SearchResults do expect(issues).to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end it 'lists confidential issues for assignee' do @@ -225,7 +201,7 @@ describe Gitlab::SearchResults do expect(issues).not_to include security_issue_3 expect(issues).to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end it 'lists confidential issues for project members' do @@ -241,7 +217,7 @@ describe Gitlab::SearchResults do expect(issues).to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 4 + expect(results.limited_issues_count).to eq 4 end it 'lists all issues for admin' do @@ -254,7 +230,7 @@ describe Gitlab::SearchResults do expect(issues).to include security_issue_3 expect(issues).to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 5 + expect(results.limited_issues_count).to eq 5 end end diff --git a/spec/lib/gitlab/string_placeholder_replacer_spec.rb b/spec/lib/gitlab/string_placeholder_replacer_spec.rb new file mode 100644 index 00000000000..7a03ea4154c --- /dev/null +++ b/spec/lib/gitlab/string_placeholder_replacer_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::StringPlaceholderReplacer do + describe '.render_url' do + it 'returns the nil if the string is blank' do + expect(described_class.replace_string_placeholders(nil, /whatever/)).to be_blank + end + + it 'returns the string if the placeholder regex' do + expect(described_class.replace_string_placeholders('whatever')).to eq 'whatever' + end + + it 'returns the string if no block given' do + expect(described_class.replace_string_placeholders('whatever', /whatever/)).to eq 'whatever' + end + + context 'when all params are valid' do + let(:string) { '%{path}/%{id}/%{branch}' } + let(:regex) { /(path|id)/ } + + it 'replaces each placeholders with the block result' do + result = described_class.replace_string_placeholders(string, regex) do |arg| + 'WHATEVER' + end + + expect(result).to eq 'WHATEVER/WHATEVER/%{branch}' + end + + it 'does not replace the placeholder if the block result is nil' do + result = described_class.replace_string_placeholders(string, regex) do |arg| + arg == 'path' ? nil : 'WHATEVER' + end + + expect(result).to eq '%{path}/WHATEVER/%{branch}' + end + end + end +end diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb index d715f9bd641..37b1298b962 100644 --- a/spec/lib/gitlab/string_regex_marker_spec.rb +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -2,17 +2,36 @@ require 'spec_helper' describe Gitlab::StringRegexMarker do describe '#mark' do - let(:raw) { %{"name": "AFNetworking"} } - let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } - subject do - described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| - %{<a href="#">#{text}</a>} + context 'with a single occurrence' do + let(:raw) { %{"name": "AFNetworking"} } + let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } + + subject do + described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| + %{<a href="#">#{text}</a>} + end + end + + it 'marks the match' do + expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>}) + expect(subject).to be_html_safe end end - it 'marks the inline diffs' do - expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>}) - expect(subject).to be_html_safe + context 'with multiple occurrences' do + let(:raw) { %{a <b> <c> d} } + let(:rich) { %{a <b> <c> d}.html_safe } + + subject do + described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:| + %{<strong>#{text}</strong>} + end + end + + it 'marks the matches' do + expect(subject).to eq(%{a <strong><b></strong> <strong><c></strong> d}) + expect(subject).to be_html_safe + end end end end diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb new file mode 100644 index 00000000000..64f3a9660e0 --- /dev/null +++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::Verify::LfsObjects do + include GitlabVerifyHelpers + + it_behaves_like 'Gitlab::Verify::BatchVerifier subclass' do + let!(:objects) { create_list(:lfs_object, 3, :with_file) } + end + + describe '#run_batches' do + let(:failures) { collect_failures } + let(:failure) { failures[lfs_object] } + + let!(:lfs_object) { create(:lfs_object, :with_file, :correct_oid) } + + it 'passes LFS objects with the correct file' do + expect(failures).to eq({}) + end + + it 'fails LFS objects with a missing file' do + FileUtils.rm_f(lfs_object.file.path) + + expect(failures.keys).to contain_exactly(lfs_object) + expect(failure).to be_a(Errno::ENOENT) + expect(failure.to_s).to include(lfs_object.file.path) + end + + it 'fails LFS objects with a mismatched oid' do + File.truncate(lfs_object.file.path, 0) + + expect(failures.keys).to contain_exactly(lfs_object) + expect(failure.to_s).to include('Checksum mismatch') + end + end +end diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb new file mode 100644 index 00000000000..6146ce61226 --- /dev/null +++ b/spec/lib/gitlab/verify/uploads_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Gitlab::Verify::Uploads do + include GitlabVerifyHelpers + + it_behaves_like 'Gitlab::Verify::BatchVerifier subclass' do + let(:projects) { create_list(:project, 3, :with_avatar) } + let!(:objects) { projects.flat_map(&:uploads) } + end + + describe '#run_batches' do + let(:project) { create(:project, :with_avatar) } + let(:failures) { collect_failures } + let(:failure) { failures[upload] } + + let!(:upload) { project.uploads.first } + + it 'passes uploads with the correct file' do + expect(failures).to eq({}) + end + + it 'fails uploads with a missing file' do + FileUtils.rm_f(upload.absolute_path) + + expect(failures.keys).to contain_exactly(upload) + expect(failure).to be_a(Errno::ENOENT) + expect(failure.to_s).to include(upload.absolute_path) + end + + it 'fails uploads with a mismatched checksum' do + upload.update!(checksum: 'something incorrect') + + expect(failures.keys).to contain_exactly(upload) + expect(failure.to_s).to include('Checksum mismatch') + end + + it 'fails uploads with a missing precalculated checksum' do + upload.update!(checksum: '') + + expect(failures.keys).to contain_exactly(upload) + expect(failure.to_s).to include('Checksum missing') + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index bcbb9287199..83c33797bbc 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -457,7 +457,7 @@ describe Notify do it 'has the correct subject and body' do is_expected.to have_subject("#{project.name} | Project was moved") - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text(project.ssh_url_to_repo) end end @@ -483,8 +483,8 @@ describe Notify do to_emails = subject.header[:to].addrs.map(&:address) expect(to_emails).to eq([recipient.notification_email]) - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Request to join the #{project.full_name} project" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project_project_members_url(project) is_expected.to have_body_text project_member.human_access end @@ -503,8 +503,8 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do - is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Access to the #{project.full_name} project was denied" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url end end @@ -520,8 +520,8 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do - is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Access to the #{project.full_name} project was granted" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.human_access end @@ -550,8 +550,8 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do - is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Invitation to join the #{project.full_name} project" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.human_access is_expected.to have_body_text project_member.invite_token @@ -575,7 +575,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.invite_email is_expected.to have_html_escaped_body_text invited_user.name @@ -598,7 +598,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.invite_email end diff --git a/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb new file mode 100644 index 00000000000..c18ae3b76d3 --- /dev/null +++ b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180306074045_migrate_create_trace_artifact_sidekiq_queue.rb') + +describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do + include Gitlab::Database::MigrationHelpers + + context 'when there are jobs in the queues' do + it 'correctly migrates queue when migrating up' do + Sidekiq::Testing.disable! do + stubbed_worker(queue: 'pipeline_default:create_trace_artifact').perform_async('Something', [1]) + stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1]) + + described_class.new.up + + expect(sidekiq_queue_length('pipeline_default:create_trace_artifact')).to eq 0 + expect(sidekiq_queue_length('pipeline_background:archive_trace')).to eq 2 + end + end + + it 'does not affect other queues under the same namespace' do + Sidekiq::Testing.disable! do + stubbed_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1]) + stubbed_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1]) + stubbed_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1]) + stubbed_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1]) + stubbed_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1]) + + described_class.new.up + + expect(sidekiq_queue_length('pipeline_default:build_coverage')).to eq 1 + expect(sidekiq_queue_length('pipeline_default:build_trace_sections')).to eq 1 + expect(sidekiq_queue_length('pipeline_default:pipeline_metrics')).to eq 1 + expect(sidekiq_queue_length('pipeline_default:pipeline_notification')).to eq 1 + expect(sidekiq_queue_length('pipeline_default:update_head_pipeline_for_merge_request')).to eq 1 + end + end + + it 'correctly migrates queue when migrating down' do + Sidekiq::Testing.disable! do + stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1]) + + described_class.new.down + + expect(sidekiq_queue_length('pipeline_default:create_trace_artifact')).to eq 1 + expect(sidekiq_queue_length('pipeline_background:archive_trace')).to eq 0 + end + end + end + + context 'when there are no jobs in the queues' do + it 'does not raise error when migrating up' do + expect { described_class.new.up }.not_to raise_error + end + + it 'does not raise error when migrating down' do + expect { described_class.new.down }.not_to raise_error + end + end + + def stubbed_worker(queue:) + Class.new do + include Sidekiq::Worker + sidekiq_options queue: queue + end + end +end diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb new file mode 100644 index 00000000000..33dc19e3432 --- /dev/null +++ b/spec/models/badge_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Badge do + let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' } + + describe 'validations' do + # Requires the let variable url_sym + shared_examples 'placeholder url' do + let(:badge) { build(:badge) } + + it 'allows url with http protocol' do + badge[url_sym] = 'http://www.example.com' + + expect(badge).to be_valid + end + + it 'allows url with https protocol' do + badge[url_sym] = 'https://www.example.com' + + expect(badge).to be_valid + end + + it 'cannot be empty' do + badge[url_sym] = '' + + expect(badge).not_to be_valid + end + + it 'cannot be nil' do + badge[url_sym] = nil + + expect(badge).not_to be_valid + end + + it 'accept badges placeholders' do + badge[url_sym] = placeholder_url + + expect(badge).to be_valid + end + + it 'sanitize url' do + badge[url_sym] = 'javascript:alert(1)' + + expect(badge).not_to be_valid + end + end + + context 'link_url format' do + let(:url_sym) { :link_url } + + it_behaves_like 'placeholder url' + end + + context 'image_url format' do + let(:url_sym) { :image_url } + + it_behaves_like 'placeholder url' + end + end + + shared_examples 'rendered_links' do + it 'should use the project information to populate the url placeholders' do + stub_project_commit_info(project) + + expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever" + end + + it 'returns the url if the project used is nil' do + expect(badge.public_send("rendered_#{method}", nil)).to eq placeholder_url + end + + def stub_project_commit_info(project) + allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever')) + allow(project).to receive(:default_branch).and_return('master') + end + end + + context 'methods' do + let(:badge) { build(:badge, link_url: placeholder_url, image_url: placeholder_url) } + let!(:project) { create(:project) } + + context '#rendered_link_url' do + let(:method) { :link_url } + + it_behaves_like 'rendered_links' + end + + context '#rendered_image_url' do + let(:method) { :image_url } + + it_behaves_like 'rendered_links' + end + end +end diff --git a/spec/models/badges/group_badge_spec.rb b/spec/models/badges/group_badge_spec.rb new file mode 100644 index 00000000000..ed7f83d0489 --- /dev/null +++ b/spec/models/badges/group_badge_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe GroupBadge do + describe 'associations' do + it { is_expected.to belong_to(:group) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:group) } + end +end diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb new file mode 100644 index 00000000000..0e1a8159cb6 --- /dev/null +++ b/spec/models/badges/project_badge_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe ProjectBadge do + let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' } + + describe 'associations' do + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + end + + shared_examples 'rendered_links' do + it 'should use the badge project information to populate the url placeholders' do + stub_project_commit_info(project) + + expect(badge.public_send("rendered_#{method}")).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever" + end + + def stub_project_commit_info(project) + allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever')) + allow(project).to receive(:default_branch).and_return('master') + end + end + + context 'methods' do + let(:badge) { build(:project_badge, link_url: placeholder_url, image_url: placeholder_url) } + let!(:project) { badge.project } + + context '#rendered_link_url' do + let(:method) { :link_url } + + it_behaves_like 'rendered_links' + end + + context '#rendered_image_url' do + let(:method) { :image_url } + + it_behaves_like 'rendered_links' + end + end +end diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 612a3c8e413..a574779e39d 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -34,6 +34,8 @@ describe Clusters::Applications::Runner do is_expected.to include('checkInterval') is_expected.to include('rbac') is_expected.to include('runners') + is_expected.to include('privileged: true') + is_expected.to include('image: ubuntu:16.04') is_expected.to include('resources') is_expected.to include("runnerToken: #{ci_runner.token}") is_expected.to include("gitlabUrl: #{Gitlab::Routing.url_helpers.root_url}") @@ -65,5 +67,33 @@ describe Clusters::Applications::Runner do expect(gitlab_runner.runner).not_to be_nil end end + + context 'with duplicated values on vendor/runner/values.yaml' do + let(:values) do + { + "concurrent" => 4, + "checkInterval" => 3, + "rbac" => { + "create" => false + }, + "clusterWideAccess" => false, + "runners" => { + "privileged" => false, + "image" => "ubuntu:16.04", + "builds" => {}, + "services" => {}, + "helpers" => {} + } + } + end + + before do + allow(gitlab_runner).to receive(:chart_values).and_return(values) + end + + it 'should overwrite values.yaml' do + is_expected.to include("privileged: #{gitlab_runner.privileged}") + end + end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 4f16b73ef38..abfc0896a41 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -18,6 +18,7 @@ describe Group do it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_one(:chat_team) } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } + it { is_expected.to have_many(:badges).class_name('GroupBadge') } describe '#members & #requesters' do let(:requester) { create(:user) } diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 04440d890aa..e66109fd98f 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -47,7 +47,7 @@ describe AsanaService do it 'calls Asana service to create a story' do data = create_data_for_commits('Message from commit. related to #123456') - expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" + expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" d1 = double('Asana::Task') expect(d1).to receive(:add_comment).with(text: expected_message) diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 23db29cb541..3e2a166cdd6 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -29,7 +29,7 @@ describe HipchatService do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } - let(:project_name) { project.name_with_namespace.gsub(/\s/, '') } + let(:project_name) { project.full_name.gsub(/\s/, '') } let(:token) { 'verySecret' } let(:server_url) { 'https://hipchat.example.com'} let(:push_sample_data) do @@ -303,7 +303,7 @@ describe HipchatService do message = hipchat.__send__(:create_pipeline_message, data) project_url = project.web_url - project_name = project.name_with_namespace.gsub(/\s/, '') + project_name = project.full_name.gsub(/\s/, '') pipeline_attributes = data[:object_attributes] ref = pipeline_attributes[:ref] ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 748c366efca..54ef0be67ff 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -166,7 +166,6 @@ describe JiraService do # Creates comment expect(WebMock).to have_requested(:post, @comment_url) - # Creates Remote Link in JIRA issue fields expect(WebMock).to have_requested(:post, @remote_link_url).with( body: hash_including( @@ -174,7 +173,7 @@ describe JiraService do object: { url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}", title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: true } } ) diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 522cf15f3ba..a5bdf9a9337 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -31,10 +31,10 @@ describe MattermostSlashCommandsService do url: 'http://trigger.url', icon_url: 'http://icon.url/icon.png', auto_complete: true, - auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}", + auto_complete_desc: "Perform common operations on: #{project.full_name}", auto_complete_hint: '[help]', - description: "Perform common operations on: #{project.name_with_namespace}", - display_name: "GitLab / #{project.name_with_namespace}", + description: "Perform common operations on: #{project.full_name}", + display_name: "GitLab / #{project.full_name}", method: 'P', username: 'GitLab' }.to_json) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f4faec9e52a..b1c9e6754b9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -80,6 +80,7 @@ describe Project do it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } + it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') } it { is_expected.to have_many(:lfs_file_locks) } context 'after initialized' do @@ -517,6 +518,20 @@ describe Project do it 'returns the project\'s last update date if it has no events' do expect(project.last_activity_date).to eq(project.updated_at) end + + it 'returns the most recent timestamp' do + project.update_attributes(updated_at: nil, + last_activity_at: timestamp, + last_repository_updated_at: timestamp - 1.hour) + + expect(project.last_activity_date).to eq(timestamp) + + project.update_attributes(updated_at: timestamp, + last_activity_at: timestamp - 1.hour, + last_repository_updated_at: nil) + + expect(project.last_activity_date).to eq(timestamp) + end end end @@ -3331,4 +3346,36 @@ describe Project do end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError end end + + describe '#badges' do + let(:project_group) { create(:group) } + let(:project) { create(:project, path: 'avatar', namespace: project_group) } + + before do + create_list(:project_badge, 2, project: project) + create(:group_badge, group: project_group) + end + + it 'returns the project and the project group badges' do + create(:group_badge, group: create(:group)) + + expect(Badge.count).to eq 4 + expect(project.badges.count).to eq 3 + end + + if Group.supports_nested_groups? + context 'with nested_groups' do + let(:parent_group) { create(:group) } + + before do + create_list(:group_badge, 2, group: project_group) + project_group.update(parent: parent_group) + end + + it 'returns the project and the project nested groups badges' do + expect(project.badges.count).to eq 5 + end + end + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 1e7671476f1..8b4b5873704 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -14,13 +14,13 @@ describe ProjectWiki do it { is_expected.to delegate_method(:repository_storage_path).to :project } it { is_expected.to delegate_method(:hashed_storage?).to :project } - describe "#path_with_namespace" do + describe "#full_path" do it "returns the project path with namespace with the .wiki extension" do - expect(subject.path_with_namespace).to eq(project.full_path + '.wiki') + expect(subject.full_path).to eq(project.full_path + '.wiki') end it 'returns the same value as #full_path' do - expect(subject.path_with_namespace).to eq(subject.full_path) + expect(subject.full_path).to eq(subject.full_path) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3531de244bd..00b5226d874 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1635,6 +1635,32 @@ describe User do end end + describe '#authorizations_for_projects' do + let!(:user) { create(:user) } + subject { Project.where("EXISTS (?)", user.authorizations_for_projects) } + + it 'includes projects that belong to a user, but no other projects' do + owned = create(:project, :private, namespace: user.namespace) + member = create(:project, :private).tap { |p| p.add_master(user) } + other = create(:project) + + expect(subject).to include(owned) + expect(subject).to include(member) + expect(subject).not_to include(other) + end + + it 'includes projects a user has access to, but no other projects' do + other_user = create(:user) + accessible = create(:project, :private, namespace: other_user.namespace) do |project| + project.add_developer(user) + end + other = create(:project) + + expect(subject).to include(accessible) + expect(subject).not_to include(other) + end + end + describe '#authorized_projects', :delete do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb new file mode 100644 index 00000000000..ae64a9ca162 --- /dev/null +++ b/spec/requests/api/badges_spec.rb @@ -0,0 +1,367 @@ +require 'spec_helper' + +describe API::Badges do + let(:master) { create(:user, username: 'master_user') } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + let(:project_group) { create(:group) } + let(:project) { setup_project } + let!(:group) { setup_group } + + shared_context 'source helpers' do + def get_source(source_type) + source_type == 'project' ? project : group + end + end + + shared_examples 'GET /:sources/:id/badges' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges", stranger) } + end + + %i[master developer access_requester stranger].each do |type| + context "when authenticated as a #{type}" do + it 'returns 200' do + user = public_send(type) + badges_count = source_type == 'project' ? 3 : 2 + + get api("/#{source_type.pluralize}/#{source.id}/badges", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(badges_count) + end + end + end + + it 'avoids N+1 queries' do + # Establish baseline + get api("/#{source_type.pluralize}/#{source.id}/badges", master) + + control = ActiveRecord::QueryRecorder.new do + get api("/#{source_type.pluralize}/#{source.id}/badges", master) + end + + project.add_developer(create(:user)) + + expect do + get api("/#{source_type.pluralize}/#{source.id}/badges", master) + end.not_to exceed_query_limit(control) + end + end + end + + shared_examples 'GET /:sources/:id/badges/:badge_id' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member' do + %i[master developer access_requester stranger].each do |type| + let(:badge) { source.badges.first } + + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + + get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(badge.id) + expect(json_response['link_url']).to eq(badge.link_url) + expect(json_response['rendered_link_url']).to eq(badge.rendered_link_url) + expect(json_response['image_url']).to eq(badge.image_url) + expect(json_response['rendered_image_url']).to eq(badge.rendered_image_url) + expect(json_response['kind']).to eq source_type + end + end + end + end + end + end + + shared_examples 'POST /:sources/:id/badges' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + let(:example_url) { 'http://www.example.com' } + let(:example_url2) { 'http://www.example1.com' } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + post api("/#{source_type.pluralize}/#{source.id}/badges", stranger), + link_url: example_url, image_url: example_url2 + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + post api("/#{source_type.pluralize}/#{source.id}/badges", user), + link_url: example_url, image_url: example_url2 + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'creates a new badge' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + link_url: example_url, image_url: example_url2 + + expect(response).to have_gitlab_http_status(201) + end.to change { source.badges.count }.by(1) + + expect(json_response['link_url']).to eq(example_url) + expect(json_response['image_url']).to eq(example_url2) + expect(json_response['kind']).to eq source_type + end + end + + it 'returns 400 when link_url is not given' do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + link_url: example_url + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when image_url is not given' do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + image_url: example_url2 + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when link_url or image_url is not valid' do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + link_url: 'whatever', image_url: 'whatever' + + expect(response).to have_gitlab_http_status(400) + end + end + end + + shared_examples 'PUT /:sources/:id/badges/:badge_id' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + let(:badge) { source.badges.first } + let(:example_url) { 'http://www.example.com' } + let(:example_url2) { 'http://www.example1.com' } + + it_behaves_like 'a 404 response when source is private' do + let(:route) do + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger), + link_url: example_url + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user), + link_url: example_url + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'updates the member' do + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master), + link_url: example_url, image_url: example_url2 + + expect(response).to have_gitlab_http_status(200) + expect(json_response['link_url']).to eq(example_url) + expect(json_response['image_url']).to eq(example_url2) + expect(json_response['kind']).to eq source_type + end + end + + it 'returns 400 when link_url or image_url is not valid' do + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master), + link_url: 'whatever', image_url: 'whatever' + + expect(response).to have_gitlab_http_status(400) + end + end + end + + shared_examples 'DELETE /:sources/:id/badges/:badge_id' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + let(:badge) { source.badges.first } + + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester developer stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user) + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'deletes the badge' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master) + + expect(response).to have_gitlab_http_status(204) + end.to change { source.badges.count }.by(-1) + end + + it_behaves_like '412 response' do + let(:request) { api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master) } + end + end + + it 'returns 404 if badge does not exist' do + delete api("/#{source_type.pluralize}/#{source.id}/badges/123", master) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + shared_examples 'GET /:sources/:id/badges/render' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + let(:example_url) { 'http://www.example.com' } + let(:example_url2) { 'http://www.example1.com' } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", stranger) + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", user) + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'gets the rendered badge values' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", master) + + expect(response).to have_gitlab_http_status(200) + + expect(json_response.keys).to contain_exactly('link_url', 'rendered_link_url', 'image_url', 'rendered_image_url') + expect(json_response['link_url']).to eq(example_url) + expect(json_response['image_url']).to eq(example_url2) + expect(json_response['rendered_link_url']).to eq(example_url) + expect(json_response['rendered_image_url']).to eq(example_url2) + end + end + + it 'returns 400 when link_url is not given' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}", master) + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when image_url is not given' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?image_url=#{example_url}", master) + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when link_url or image_url is not valid' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=whatever&image_url=whatever", master) + + expect(response).to have_gitlab_http_status(400) + end + end + end + + context 'when deleting a badge' do + context 'and the source is a project' do + it 'cannot delete badges owned by the project group' do + delete api("/projects/#{project.id}/badges/#{project_group.badges.first.id}", master) + + expect(response).to have_gitlab_http_status(403) + end + end + end + + describe 'Endpoints' do + %w(project group).each do |source_type| + it_behaves_like 'GET /:sources/:id/badges', source_type + it_behaves_like 'GET /:sources/:id/badges/:badge_id', source_type + it_behaves_like 'GET /:sources/:id/badges/render', source_type + it_behaves_like 'POST /:sources/:id/badges', source_type + it_behaves_like 'PUT /:sources/:id/badges/:badge_id', source_type + it_behaves_like 'DELETE /:sources/:id/badges/:badge_id', source_type + end + end + + def setup_project + create(:project, :public, :access_requestable, creator_id: master.id, namespace: project_group) do |project| + project.add_developer(developer) + project.add_master(master) + project.request_access(access_requester) + project.project_badges << build(:project_badge, project: project) + project.project_badges << build(:project_badge, project: project) + project_group.badges << build(:group_badge, group: group) + end + end + + def setup_group + create(:group, :public, :access_requestable) do |group| + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + group.badges << build(:group_badge, group: group) + group.badges << build(:group_badge, group: group) + end + end +end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index e433597f58b..64f51d9843d 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -39,6 +39,27 @@ describe API::Branches do end end + context 'when search parameter is passed' do + context 'and branch exists' do + it 'returns correct branches' do + get api(route, user), per_page: 100, search: branch_name + + searched_branch_names = json_response.map { |branch| branch['name'] } + project_branch_names = project.repository.branch_names.grep(/#{branch_name}/) + + expect(searched_branch_names).to match_array(project_branch_names) + end + end + + context 'and branch does not exist' do + it 'returns an empty array' do + get api(route, user), per_page: 100, search: 'no_such_branch_name_entropy_of_jabadabadu' + + expect(json_response).to eq [] + end + end + end + context 'when unauthenticated', 'and project is public' do before do project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index d1569e5d650..6614e8cea43 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -163,6 +163,42 @@ describe API::Issues do expect(first_issue['id']).to eq(issue.id) end + context 'filtering before a specific date' do + let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) } + + it 'returns issues created before a specific date' do + get api('/issues?created_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues updated before a specific date' do + get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + end + + context 'filtering after a specific date' do + let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) } + + it 'returns issues created after a specific date' do + get api("/issues?created_after=#{issue2.created_at}", user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues updated after a specific date' do + get api("/issues?updated_after=#{issue2.updated_at}", user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + end + it 'returns an array of labeled issues' do get api("/issues", user), labels: label.title diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e8eb01f6c32..484322752c0 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -172,6 +172,42 @@ describe API::MergeRequests do end end + it 'returns merge requests created before a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', created_at: Date.new(2000, 1, 1)) + + get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + + it 'returns merge requests created after a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', created_at: 1.week.from_now) + + get api("/merge_requests?created_after=#{merge_request2.created_at}", user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + + it 'returns merge requests updated before a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', updated_at: Date.new(2000, 1, 1)) + + get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + + it 'returns merge requests updated after a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', updated_at: 1.week.from_now) + + get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + context 'search params' do before do merge_request.update(title: 'Search title', description: 'Search description') diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index 025165622b7..dc3a116c060 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -16,7 +16,7 @@ describe API::PagesDomains do let(:route) { "/projects/#{project.id}/pages/domains" } let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" } - let(:route_domain_path) { "/projects/#{project.path_with_namespace.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" } + let(:route_domain_path) { "/projects/#{project.full_path.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" } let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" } let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" } let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" } diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb new file mode 100644 index 00000000000..fbed527963f --- /dev/null +++ b/spec/requests/api/project_export_spec.rb @@ -0,0 +1,290 @@ +require 'spec_helper' + +describe API::ProjectExport do + set(:project) { create(:project) } + set(:project_none) { create(:project) } + set(:project_started) { create(:project) } + set(:project_finished) { create(:project) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } + + let(:path) { "/projects/#{project.id}/export" } + let(:path_none) { "/projects/#{project_none.id}/export" } + let(:path_started) { "/projects/#{project_started.id}/export" } + let(:path_finished) { "/projects/#{project_finished.id}/export" } + + let(:download_path) { "/projects/#{project.id}/export/download" } + let(:download_path_none) { "/projects/#{project_none.id}/export/download" } + let(:download_path_started) { "/projects/#{project_started.id}/export/download" } + let(:download_path_finished) { "/projects/#{project_finished.id}/export/download" } + + let(:export_path) { "#{Dir.tmpdir}/project_export_spec" } + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + # simulate exporting work directory + FileUtils.mkdir_p File.join(project_started.export_path, 'securerandom-hex') + + # simulate exported + FileUtils.mkdir_p project_finished.export_path + FileUtils.touch File.join(project_finished.export_path, '_export.tar.gz') + end + + after do + FileUtils.rm_rf(export_path, secure: true) + end + + shared_examples_for 'when project export is disabled' do + before do + stub_application_setting(project_export_enabled?: false) + end + + it_behaves_like '404 response' + end + + describe 'GET /projects/:project_id/export' do + shared_examples_for 'get project export status not found' do + it_behaves_like '404 response' do + let(:request) { get api(path, user) } + end + end + + shared_examples_for 'get project export status denied' do + it_behaves_like '403 response' do + let(:request) { get api(path, user) } + end + end + + shared_examples_for 'get project export status ok' do + it 'is none' do + get api(path_none, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/project/export_status') + expect(json_response['export_status']).to eq('none') + end + + it 'is started' do + get api(path_started, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/project/export_status') + expect(json_response['export_status']).to eq('started') + end + + it 'is finished' do + get api(path_finished, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/project/export_status') + expect(json_response['export_status']).to eq('finished') + end + end + + it_behaves_like 'when project export is disabled' do + let(:request) { get api(path, admin) } + end + + context 'when project export is enabled' do + context 'when user is an admin' do + let(:user) { admin } + + it_behaves_like 'get project export status ok' + end + + context 'when user is a master' do + before do + project.add_master(user) + project_none.add_master(user) + project_started.add_master(user) + project_finished.add_master(user) + end + + it_behaves_like 'get project export status ok' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like 'get project export status denied' + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like 'get project export status denied' + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like 'get project export status denied' + end + + context 'when user is not a member' do + it_behaves_like 'get project export status not found' + end + end + end + + describe 'GET /projects/:project_id/export/download' do + shared_examples_for 'get project export download not found' do + it_behaves_like '404 response' do + let(:request) { get api(download_path, user) } + end + end + + shared_examples_for 'get project export download denied' do + it_behaves_like '403 response' do + let(:request) { get api(download_path, user) } + end + end + + shared_examples_for 'get project export download' do + it_behaves_like '404 response' do + let(:request) { get api(download_path_none, user) } + end + + it_behaves_like '404 response' do + let(:request) { get api(download_path_started, user) } + end + + it 'downloads' do + get api(download_path_finished, user) + + expect(response).to have_gitlab_http_status(200) + end + end + + it_behaves_like 'when project export is disabled' do + let(:request) { get api(download_path, admin) } + end + + context 'when project export is enabled' do + context 'when user is an admin' do + let(:user) { admin } + + it_behaves_like 'get project export download' + end + + context 'when user is a master' do + before do + project.add_master(user) + project_none.add_master(user) + project_started.add_master(user) + project_finished.add_master(user) + end + + it_behaves_like 'get project export download' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like 'get project export download denied' + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like 'get project export download denied' + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like 'get project export download denied' + end + + context 'when user is not a member' do + it_behaves_like 'get project export download not found' + end + end + end + + describe 'POST /projects/:project_id/export' do + shared_examples_for 'post project export start not found' do + it_behaves_like '404 response' do + let(:request) { post api(path, user) } + end + end + + shared_examples_for 'post project export start denied' do + it_behaves_like '403 response' do + let(:request) { post api(path, user) } + end + end + + shared_examples_for 'post project export start' do + it 'starts' do + post api(path, user) + + expect(response).to have_gitlab_http_status(202) + end + end + + it_behaves_like 'when project export is disabled' do + let(:request) { post api(path, admin) } + end + + context 'when project export is enabled' do + context 'when user is an admin' do + let(:user) { admin } + + it_behaves_like 'post project export start' + end + + context 'when user is a master' do + before do + project.add_master(user) + project_none.add_master(user) + project_started.add_master(user) + project_finished.add_master(user) + end + + it_behaves_like 'post project export start' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like 'post project export start denied' + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like 'post project export start denied' + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like 'post project export start denied' + end + + context 'when user is not a member' do + it_behaves_like 'post project export start not found' + end + end + end +end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 72cafac3f90..95c23726a79 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -698,10 +698,10 @@ describe API::Runner do end end - context 'when tace is given' do + context 'when trace is given' do it 'creates a trace artifact' do allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do - CreateTraceArtifactWorker.new.perform(job.id) + ArchiveTraceWorker.new.perform(job.id) end update_job(state: 'success', trace: 'BUILD TRACE UPDATED') @@ -1100,11 +1100,13 @@ describe API::Runner do context 'posts artifacts file and metadata file' do let!(:artifacts) { file_upload } + let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest } let!(:metadata) { file_upload2 } let(:stored_artifacts_file) { job.reload.artifacts_file.file } let(:stored_metadata_file) { job.reload.artifacts_metadata.file } let(:stored_artifacts_size) { job.reload.artifacts_size } + let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 } before do post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token) @@ -1114,6 +1116,7 @@ describe API::Runner do let(:post_data) do { 'file.path' => artifacts.path, 'file.name' => artifacts.original_filename, + 'file.sha256' => artifacts_sha256, 'metadata.path' => metadata.path, 'metadata.name' => metadata.original_filename } end @@ -1123,6 +1126,7 @@ describe API::Runner do expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) expect(stored_artifacts_size).to eq(72821) + expect(stored_artifacts_sha256).to eq(artifacts_sha256) end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 0467e0251b3..6dbbb1ad7bb 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -795,9 +795,9 @@ describe 'Git HTTP requests' do let(:path) { 'doesnt/exist.git' } before do - allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true) - allow(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil) - allow(Gitlab::Auth::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user) + allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_return(true) + allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil) + allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user) end it_behaves_like 'pulls require Basic HTTP Authentication' diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 9128280eb5a..290eeae828e 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -172,7 +172,7 @@ describe Auth::ContainerRegistryAuthenticationService do end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -200,7 +200,7 @@ describe Auth::ContainerRegistryAuthenticationService do end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -239,7 +239,7 @@ describe Auth::ContainerRegistryAuthenticationService do end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -270,7 +270,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to delete images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -311,7 +311,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to delete images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -323,7 +323,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to pull or push images' do let(:current_user) { create(:user, external: true) } let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + { scope: "repository:#{project.full_path}:pull,push" } end it_behaves_like 'an inaccessible' @@ -333,7 +333,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to delete images' do let(:current_user) { create(:user, external: true) } let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -359,7 +359,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'allow to delete images' do let(:current_params) do - { scope: "repository:#{current_project.path_with_namespace}:*" } + { scope: "repository:#{current_project.full_path}:*" } end it_behaves_like 'a deletable' do @@ -398,7 +398,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow to delete images' do let(:current_params) do - { scope: "repository:#{current_project.path_with_namespace}:*" } + { scope: "repository:#{current_project.full_path}:*" } end it_behaves_like 'an inaccessible' do diff --git a/spec/services/ci/create_trace_artifact_service_spec.rb b/spec/services/ci/create_trace_artifact_service_spec.rb deleted file mode 100644 index 8c5e8e438c7..00000000000 --- a/spec/services/ci/create_trace_artifact_service_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'spec_helper' - -describe Ci::CreateTraceArtifactService do - describe '#execute' do - subject { described_class.new(nil, nil).execute(job) } - - context 'when the job does not have trace artifact' do - context 'when the job has a trace file' do - let!(:job) { create(:ci_build, :trace_live) } - let!(:legacy_path) { job.trace.read { |stream| return stream.path } } - let!(:legacy_checksum) { Digest::SHA256.file(legacy_path).hexdigest } - let(:new_path) { job.job_artifacts_trace.file.path } - let(:new_checksum) { Digest::SHA256.file(new_path).hexdigest } - - it { expect(File.exist?(legacy_path)).to be_truthy } - - it 'creates trace artifact' do - expect { subject }.to change { Ci::JobArtifact.count }.by(1) - - expect(File.exist?(legacy_path)).to be_falsy - expect(File.exist?(new_path)).to be_truthy - expect(new_checksum).to eq(legacy_checksum) - expect(job.job_artifacts_trace.file.exists?).to be_truthy - expect(job.job_artifacts_trace.file.filename).to eq('job.log') - end - - context 'when failed to create trace artifact record' do - before do - # When ActiveRecord error happens - allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false) - allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages) - .and_return("Error") - - subject rescue nil - - job.reload - end - - it 'keeps legacy trace and removes trace artifact' do - expect(File.exist?(legacy_path)).to be_truthy - expect(job.job_artifacts_trace).to be_nil - end - end - end - - context 'when the job does not have a trace file' do - let!(:job) { create(:ci_build) } - - it 'does not create trace artifact' do - expect { subject }.not_to change { Ci::JobArtifact.count } - end - end - end - - context 'when the job has already had trace artifact' do - let!(:job) { create(:ci_build, :trace_artifact) } - - it 'does not create trace artifact' do - expect { subject }.not_to change { Ci::JobArtifact.count } - end - end - end -end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 5d226f34d2d..44a83c436cb 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -28,6 +28,7 @@ describe MergeRequests::CreateService do it 'creates an MR' do expect(merge_request).to be_valid + expect(merge_request.work_in_progress?).to be(false) expect(merge_request.title).to eq('Awesome merge_request') expect(merge_request.assignee).to be_nil expect(merge_request.merge_params['force_remove_source_branch']).to eq('1') @@ -62,6 +63,40 @@ describe MergeRequests::CreateService do expect(Event.where(attributes).count).to eq(1) end + describe 'when marked with /wip' do + context 'in title and in description' do + let(:opts) do + { + title: 'WIP: Awesome merge_request', + description: "well this is not done yet\n/wip", + source_branch: 'feature', + target_branch: 'master', + assignee: assignee + } + end + + it 'sets MR to WIP' do + expect(merge_request.work_in_progress?).to be(true) + end + end + + context 'in description only' do + let(:opts) do + { + title: 'Awesome merge_request', + description: "well this is not done yet\n/wip", + source_branch: 'feature', + target_branch: 'master', + assignee: assignee + } + end + + it 'sets MR to WIP' do + expect(merge_request.work_in_progress?).to be(true) + end + end + end + context 'when merge request is assigned to someone' do let(:opts) do { diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 0ae26e87154..f5cff66de6d 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -57,32 +57,55 @@ describe Notes::CreateService do end end - describe 'note with commands' do - describe '/close, /label, /assign & /milestone' do - let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) } + context 'note with commands' do + context 'as a user who can update the target' do + context '/close, /label, /assign & /milestone' do + let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) } - it 'saves the note and does not alter the note text' do - expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original + it 'saves the note and does not alter the note text' do + expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original - note = described_class.new(project, user, opts.merge(note: note_text)).execute + note = described_class.new(project, user, opts.merge(note: note_text)).execute - expect(note.note).to eq "HELLO\nWORLD" + expect(note.note).to eq "HELLO\nWORLD" + end + end + + context '/merge with sha option' do + let(:note_text) { %(HELLO\n/merge\nWORLD) } + let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') } + + it 'saves the note and exectues merge command' do + note = described_class.new(project, user, params).execute + + expect(note.note).to eq "HELLO\nWORLD" + end end end - describe '/merge with sha option' do - let(:note_text) { %(HELLO\n/merge\nWORLD) } - let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') } + context 'as a user who cannot update the target' do + let(:note_text) { "HELLO\n/todo\n/assign #{user.to_reference}\nWORLD" } + let(:note) { described_class.new(project, user, opts.merge(note: note_text)).execute } - it 'saves the note and exectues merge command' do - note = described_class.new(project, user, params).execute + before do + project.team.find_member(user.id).update!(access_level: Gitlab::Access::GUEST) + end + + it 'applies commands the user can execute' do + expect { note }.to change { user.todos_pending_count }.from(0).to(1) + end + + it 'does not apply commands the user cannot execute' do + expect { note }.not_to change { issue.assignees } + end + it 'saves the note' do expect(note.note).to eq "HELLO\nWORLD" end end end - describe 'personal snippet note' do + context 'personal snippet note' do subject { described_class.new(nil, user, params).execute } let(:snippet) { create(:personal_snippet) } @@ -103,7 +126,7 @@ describe Notes::CreateService do end end - describe 'note with emoji only' do + context 'note with emoji only' do it 'creates regular note' do opts = { note: ':smile: ', diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 5eafe56c99d..b1e218821d2 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -165,31 +165,17 @@ describe Notes::QuickActionsService do let(:note) { create(:note_on_issue, project: project) } - context 'with no current_user' do - it 'returns false' do - expect(described_class.supported?(note, nil)).to be_falsy - end - end - - context 'when current_user cannot update the noteable' do - it 'returns false' do - user = create(:user) - - expect(described_class.supported?(note, user)).to be_falsy - end - end - - context 'when current_user can update the noteable' do + context 'with a note on an issue' do it 'returns true' do - expect(described_class.supported?(note, master)).to be_truthy + expect(described_class.supported?(note)).to be_truthy end + end - context 'with a note on a commit' do - let(:note) { create(:note_on_commit, project: project) } + context 'with a note on a commit' do + let(:note) { create(:note_on_commit, project: project) } - it 'returns false' do - expect(described_class.supported?(note, nil)).to be_falsy - end + it 'returns false' do + expect(described_class.supported?(note)).to be_falsy end end end @@ -201,7 +187,7 @@ describe Notes::QuickActionsService do service = described_class.new(project, master) note = create(:note_on_issue, project: project) - expect(described_class).to receive(:supported?).with(note, master) + expect(described_class).to receive(:supported?).with(note) service.supported?(note) end diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index bfb86284d86..934106627a9 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -34,6 +34,7 @@ describe Projects::UpdatePagesService do context 'with expiry date' do before do build.artifacts_expire_in = "2 days" + build.save! end it "doesn't delete artifacts" do @@ -105,6 +106,7 @@ describe Projects::UpdatePagesService do context 'with expiry date' do before do build.artifacts_expire_in = "2 days" + build.save! end it "doesn't delete artifacts" do @@ -159,6 +161,20 @@ describe Projects::UpdatePagesService do expect(execute).not_to eq(:success) end + + context 'when timeout happens by DNS error' do + before do + allow_any_instance_of(described_class) + .to receive(:extract_zip_archive!).and_raise(SocketError) + end + + it 'raises an error' do + expect { execute }.to raise_error(SocketError) + + build.reload + expect(build.artifacts?).to eq(true) + end + end end end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index c40cd5b7548..51396d34f8f 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -30,6 +30,7 @@ describe SystemHooksService do :old_path_with_namespace ) end + it do project.old_path_with_namespace = 'transfered_from_path' expect(event_data(project, :transfer)).to include( @@ -45,18 +46,21 @@ describe SystemHooksService do :owner_name, :owner_email ) end + it do expect(event_data(group, :destroy)).to include( :event_name, :name, :created_at, :updated_at, :path, :group_id, :owner_name, :owner_email ) end + it do expect(event_data(group_member, :create)).to include( :event_name, :created_at, :updated_at, :group_name, :group_path, :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end + it do expect(event_data(group_member, :destroy)).to include( :event_name, :created_at, :updated_at, :group_name, :group_path, @@ -70,6 +74,14 @@ describe SystemHooksService do expect(data[:project_visibility]).to eq('private') end + it 'handles nil datetime columns' do + user.update_attributes(created_at: nil, updated_at: nil) + data = event_data(user, :destroy) + + expect(data[:created_at]).to be(nil) + expect(data[:updated_at]).to be(nil) + end + context 'group_rename' do it 'contains old and new path' do allow(group).to receive(:path_was).and_return('old-path') diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5b5edc1aa0d..a3893188c6e 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -789,7 +789,7 @@ describe SystemNoteService do object: { url: project_commit_url(project, commit), title: "GitLab: Mentioned on commit - #{commit.title}", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: false } } ) @@ -815,7 +815,7 @@ describe SystemNoteService do object: { url: project_issue_url(project, issue), title: "GitLab: Mentioned on issue - #{issue.title}", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: false } } ) @@ -841,7 +841,7 @@ describe SystemNoteService do object: { url: project_snippet_url(project, snippet), title: "GitLab: Mentioned on snippet - #{snippet.title}", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: false } } ) diff --git a/spec/support/bare_repo_operations.rb b/spec/support/bare_repo_operations.rb index 38d11992dc2..8eeaa37d3c5 100644 --- a/spec/support/bare_repo_operations.rb +++ b/spec/support/bare_repo_operations.rb @@ -11,6 +11,14 @@ class BareRepoOperations @path_to_repo = path_to_repo end + def commit_tree(tree_id, msg, parent: EMPTY_TREE_ID) + commit_tree_args = ['commit-tree', tree_id, '-m', msg] + commit_tree_args += ['-p', parent] unless parent == EMPTY_TREE_ID + commit_id = execute(commit_tree_args) + + commit_id[0] + end + # Based on https://stackoverflow.com/a/25556917/1856239 def commit_file(file, dst_path, branch = 'master') head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || EMPTY_TREE_ID @@ -26,11 +34,9 @@ class BareRepoOperations tree_id = execute(['write-tree']) - commit_tree_args = ['commit-tree', tree_id[0], '-m', "Add #{dst_path}"] - commit_tree_args += ['-p', head_id] unless head_id == EMPTY_TREE_ID - commit_id = execute(commit_tree_args) + commit_id = commit_tree(tree_id[0], "Add #{dst_path}", parent: head_id) - execute(['update-ref', "refs/heads/#{branch}", commit_id[0]]) + execute(['update-ref', "refs/heads/#{branch}", commit_id]) end private diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 2c20821ac3f..f61469f673d 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -127,7 +127,6 @@ shared_examples 'issuable record that supports quick actions in its description it "does not close the #{issuable_type}" do write_note("/close") - expect(page).to have_content '/close' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_open @@ -165,7 +164,6 @@ shared_examples 'issuable record that supports quick actions in its description it "does not reopen the #{issuable_type}" do write_note("/reopen") - expect(page).to have_content '/reopen' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_closed @@ -195,10 +193,9 @@ shared_examples 'issuable record that supports quick actions in its description visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end - it "does not reopen the #{issuable_type}" do + it "does not change the #{issuable_type} title" do write_note("/title Awesome new title") - expect(page).to have_content '/title' expect(page).not_to have_content 'Commands applied' expect(issuable.reload.title).not_to eq 'Awesome new title' diff --git a/spec/support/gitlab_verify.rb b/spec/support/gitlab_verify.rb new file mode 100644 index 00000000000..13e2e37624d --- /dev/null +++ b/spec/support/gitlab_verify.rb @@ -0,0 +1,45 @@ +RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do + describe 'batching' do + let(:first_batch) { objects[0].id..objects[0].id } + let(:second_batch) { objects[1].id..objects[1].id } + let(:third_batch) { objects[2].id..objects[2].id } + + it 'iterates through objects in batches' do + expect(collect_ranges).to eq([first_batch, second_batch, third_batch]) + end + + it 'allows the starting ID to be specified' do + expect(collect_ranges(start: second_batch.first)).to eq([second_batch, third_batch]) + end + + it 'allows the finishing ID to be specified' do + expect(collect_ranges(finish: second_batch.last)).to eq([first_batch, second_batch]) + end + end +end + +module GitlabVerifyHelpers + def collect_ranges(args = {}) + verifier = described_class.new(args.merge(batch_size: 1)) + + collect_results(verifier).map { |range, _| range } + end + + def collect_failures + verifier = described_class.new(batch_size: 1) + + out = {} + + collect_results(verifier).map { |_, failures| out.merge!(failures) } + + out + end + + def collect_results(verifier) + out = [] + + verifier.run_batches { |*args| out << args } + + out + end +end diff --git a/spec/tasks/gitlab/lfs/check_rake_spec.rb b/spec/tasks/gitlab/lfs/check_rake_spec.rb new file mode 100644 index 00000000000..2610edf8bac --- /dev/null +++ b/spec/tasks/gitlab/lfs/check_rake_spec.rb @@ -0,0 +1,28 @@ +require 'rake_helper' + +describe 'gitlab:lfs rake tasks' do + describe 'check' do + let!(:lfs_object) { create(:lfs_object, :with_file, :correct_oid) } + + before do + Rake.application.rake_require('tasks/gitlab/lfs/check') + stub_env('VERBOSE' => 'true') + end + + it 'outputs the integrity check for each batch' do + expect { run_rake_task('gitlab:lfs:check') }.to output(/Failures: 0/).to_stdout + end + + it 'errors out about missing files on the file system' do + FileUtils.rm_f(lfs_object.file.path) + + expect { run_rake_task('gitlab:lfs:check') }.to output(/No such file.*#{Regexp.quote(lfs_object.file.path)}/).to_stdout + end + + it 'errors out about invalid checksum' do + File.truncate(lfs_object.file.path, 0) + + expect { run_rake_task('gitlab:lfs:check') }.to output(/Checksum mismatch/).to_stdout + end + end +end diff --git a/spec/tasks/gitlab/traces_rake_spec.rb b/spec/tasks/gitlab/traces_rake_spec.rb new file mode 100644 index 00000000000..bd18e8ffc1e --- /dev/null +++ b/spec/tasks/gitlab/traces_rake_spec.rb @@ -0,0 +1,55 @@ +require 'rake_helper' + +describe 'gitlab:traces rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/traces' + end + + shared_examples 'passes the job id to worker' do + it do + expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]]) + + run_rake_task('gitlab:traces:archive') + end + end + + shared_examples 'does not pass the job id to worker' do + it do + expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async) + + run_rake_task('gitlab:traces:archive') + end + end + + context 'when trace file stored in default path' do + let!(:job) { create(:ci_build, :success, :trace_live) } + + it_behaves_like 'passes the job id to worker' + end + + context 'when trace is stored in database' do + let!(:job) { create(:ci_build, :success) } + + before do + job.update_column(:trace, 'trace in db') + end + + it_behaves_like 'passes the job id to worker' + end + + context 'when job has trace artifact' do + let!(:job) { create(:ci_build, :success) } + + before do + create(:ci_job_artifact, :trace, job: job) + end + + it_behaves_like 'does not pass the job id to worker' + end + + context 'when job is not finished yet' do + let!(:build) { create(:ci_build, :running, :trace_live) } + + it_behaves_like 'does not pass the job id to worker' + end +end diff --git a/spec/tasks/gitlab/uploads_rake_spec.rb b/spec/tasks/gitlab/uploads/check_rake_spec.rb index ac0005e51e0..5d597c66133 100644 --- a/spec/tasks/gitlab/uploads_rake_spec.rb +++ b/spec/tasks/gitlab/uploads/check_rake_spec.rb @@ -5,23 +5,24 @@ describe 'gitlab:uploads rake tasks' do let!(:upload) { create(:upload, path: Rails.root.join('spec/fixtures/banana_sample.gif')) } before do - Rake.application.rake_require 'tasks/gitlab/uploads' + Rake.application.rake_require('tasks/gitlab/uploads/check') + stub_env('VERBOSE' => 'true') end - it 'outputs the integrity check for each uploaded file' do - expect { run_rake_task('gitlab:uploads:check') }.to output(/Checking file \(#{upload.id}\): #{Regexp.quote(upload.absolute_path)}/).to_stdout + it 'outputs the integrity check for each batch' do + expect { run_rake_task('gitlab:uploads:check') }.to output(/Failures: 0/).to_stdout end it 'errors out about missing files on the file system' do - create(:upload) + missing_upload = create(:upload) - expect { run_rake_task('gitlab:uploads:check') }.to output(/File does not exist on the file system/).to_stdout + expect { run_rake_task('gitlab:uploads:check') }.to output(/No such file.*#{Regexp.quote(missing_upload.absolute_path)}/).to_stdout end it 'errors out about invalid checksum' do upload.update_column(:checksum, '01a3156db2cf4f67ec823680b40b7302f89ab39179124ad219f94919b8a1769e') - expect { run_rake_task('gitlab:uploads:check') }.to output(/File checksum \(9e697aa09fe196909813ee36103e34f721fe47a5fdc8aac0e4e4ac47b9b38282\) does not match the one in the database \(#{upload.checksum}\)/).to_stdout + expect { run_rake_task('gitlab:uploads:check') }.to output(/Checksum mismatch/).to_stdout end end end diff --git a/spec/validators/url_placeholder_validator_spec.rb b/spec/validators/url_placeholder_validator_spec.rb new file mode 100644 index 00000000000..b76d8acdf88 --- /dev/null +++ b/spec/validators/url_placeholder_validator_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe UrlPlaceholderValidator do + let(:validator) { described_class.new(attributes: [:link_url], **options) } + let!(:badge) { build(:badge) } + let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' } + + subject { validator.validate_each(badge, :link_url, badge.link_url) } + + describe '#validates_each' do + context 'with no options' do + let(:options) { {} } + + it 'allows http and https protocols by default' do + expect(validator.send(:default_options)[:protocols]).to eq %w(http https) + end + + it 'checks that the url structure is valid' do + badge.link_url = placeholder_url + + subject + + expect(badge.errors.empty?).to be false + end + end + + context 'with placeholder regex' do + let(:options) { { placeholder_regex: /(project_path|project_id|commit_sha|default_branch)/ } } + + it 'checks that the url is valid and obviate placeholders that match regex' do + badge.link_url = placeholder_url + + subject + + expect(badge.errors.empty?).to be true + end + end + end +end diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb new file mode 100644 index 00000000000..763dff181d2 --- /dev/null +++ b/spec/validators/url_validator_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe UrlValidator do + let(:validator) { described_class.new(attributes: [:link_url], **options) } + let!(:badge) { build(:badge) } + + subject { validator.validate_each(badge, :link_url, badge.link_url) } + + describe '#validates_each' do + context 'with no options' do + let(:options) { {} } + + it 'allows http and https protocols by default' do + expect(validator.send(:default_options)[:protocols]).to eq %w(http https) + end + + it 'checks that the url structure is valid' do + badge.link_url = 'http://www.google.es/%{whatever}' + + subject + + expect(badge.errors.empty?).to be false + end + end + + context 'with protocols' do + let(:options) { { protocols: %w(http) } } + + it 'allows urls with the defined protocols' do + badge.link_url = 'http://www.example.com' + + subject + + expect(badge.errors.empty?).to be true + end + + it 'add error if the url protocol does not match the selected ones' do + badge.link_url = 'https://www.example.com' + + subject + + expect(badge.errors.empty?).to be false + end + end + end +end diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index 62af946dcab..15fce65979b 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe 'projects/_home_panel' do - let(:project) { create(:project, :public) } + let(:group) { create(:group) } + let(:project) { create(:project, :public, namespace: group) } let(:notification_settings) do user&.notification_settings_for(project) @@ -35,4 +36,55 @@ describe 'projects/_home_panel' do expect(rendered).not_to have_selector('.notification_dropdown') end end + + context 'when project' do + let!(:user) { create(:user) } + let(:badges) { project.badges } + + context 'has no badges' do + it 'should not render any badge' do + render + + expect(rendered).to have_selector('.project-badges') + expect(rendered).not_to have_selector('.project-badges > a') + end + end + + shared_examples 'show badges' do + it 'should render the all badges' do + render + + expect(rendered).to have_selector('.project-badges a') + + badges.each do |badge| + expect(rendered).to have_link(href: badge.rendered_link_url) + end + end + end + + context 'only has group badges' do + before do + create(:group_badge, group: project.group) + end + + it_behaves_like 'show badges' + end + + context 'only has project badges' do + before do + create(:project_badge, project: project) + end + + it_behaves_like 'show badges' + end + + context 'has both group and project badges' do + before do + create(:project_badge, project: project) + create(:group_badge, group: project.group) + end + + it_behaves_like 'show badges' + end + end end diff --git a/spec/workers/create_trace_artifact_worker_spec.rb b/spec/workers/archive_trace_worker_spec.rb index 854abd9cca7..b768588c6e1 100644 --- a/spec/workers/create_trace_artifact_worker_spec.rb +++ b/spec/workers/archive_trace_worker_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateTraceArtifactWorker do +describe ArchiveTraceWorker do describe '#perform' do subject { described_class.new.perform(job&.id) } @@ -8,8 +8,7 @@ describe CreateTraceArtifactWorker do let(:job) { create(:ci_build) } it 'executes service' do - expect_any_instance_of(Ci::CreateTraceArtifactService) - .to receive(:execute).with(job) + expect_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!) subject end @@ -19,8 +18,7 @@ describe CreateTraceArtifactWorker do let(:job) { nil } it 'does not execute service' do - expect_any_instance_of(Ci::CreateTraceArtifactService) - .not_to receive(:execute) + expect_any_instance_of(Gitlab::Ci::Trace).not_to receive(:archive!) subject end diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index c7ff8cf3b92..acd8da11d8d 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -14,7 +14,7 @@ describe BuildFinishedWorker do expect_any_instance_of(BuildTraceSectionsWorker).to receive(:perform) expect_any_instance_of(BuildCoverageWorker).to receive(:perform) expect(BuildHooksWorker).to receive(:perform_async) - expect(CreateTraceArtifactWorker).to receive(:perform_async) + expect(ArchiveTraceWorker).to receive(:perform_async) described_class.new.perform(build.id) end diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb index 68cfe9d5545..615462380e0 100644 --- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::GithubImport::ObjectImporter do importer_class = double(:importer_class) importer_instance = double(:importer_instance) representation = double(:representation) - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) expect(worker) diff --git a/spec/workers/concerns/pipeline_background_queue_spec.rb b/spec/workers/concerns/pipeline_background_queue_spec.rb new file mode 100644 index 00000000000..24c0a3c6a20 --- /dev/null +++ b/spec/workers/concerns/pipeline_background_queue_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe PipelineBackgroundQueue do + let(:worker) do + Class.new do + def self.name + 'DummyWorker' + end + + include ApplicationWorker + include PipelineBackgroundQueue + end + end + + it 'sets a default object storage queue automatically' do + expect(worker.sidekiq_options['queue']) + .to eq 'pipeline_background:dummy' + end +end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 47297de738b..74539a7e493 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -195,6 +195,12 @@ describe GitGarbageCollectWorker do expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled) end + + it 'cleans up repository after finishing' do + expect_any_instance_of(Project).to receive(:cleanup).and_call_original + + subject.perform(project.id, 'gc', lease_key, lease_uuid) + end end context 'with bitmaps enabled' do diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb index 7c8c665a9b3..48e7eaf32fc 100644 --- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportDiffNoteWorker do describe '#import' do it 'imports a diff note' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb index 4116380ff4d..8cf6ac15919 100644 --- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportIssueWorker do describe '#import' do it 'imports an issue' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb index 0ca825a722b..677697c02df 100644 --- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportNoteWorker do describe '#import' do it 'imports a note' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb index d49f560af42..e287ddbe0d7 100644 --- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportPullRequestWorker do describe '#import' do it 'imports a pull request' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 76ef57b6b1e..ac79d9c0ac1 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -20,32 +20,6 @@ describe ProcessCommitWorker do worker.perform(project.id, -1, commit.to_hash) end - context 'when commit is a merge request merge commit' do - let(:merge_request) do - create(:merge_request, - description: "Closes #{issue.to_reference}", - source_branch: 'feature-merged', - target_branch: 'master', - source_project: project) - end - - let(:commit) do - project.repository.create_branch('feature-merged', 'feature') - - sha = project.repository.merge(user, - merge_request.diff_head_sha, - merge_request, - "Closes #{issue.to_reference}") - project.repository.commit(sha) - end - - it 'it does not close any issues from the commit message' do - expect(worker).not_to receive(:close_issues) - - worker.perform(project.id, user.id, commit.to_hash) - end - end - it 'processes the commit message' do expect(worker).to receive(:process_commit_message).and_call_original @@ -73,13 +47,21 @@ describe ProcessCommitWorker do describe '#process_commit_message' do context 'when pushing to the default branch' do - it 'closes issues that should be closed per the commit message' do + before do allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") + end + it 'closes issues that should be closed per the commit message' do expect(worker).to receive(:close_issues).with(project, user, user, commit, [issue]) worker.process_commit_message(project, commit, user, user, true) end + + it 'creates cross references' do + expect(commit).to receive(:create_cross_references!).with(user, [issue]) + + worker.process_commit_message(project, commit, user, user, true) + end end context 'when pushing to a non-default branch' do @@ -90,12 +72,44 @@ describe ProcessCommitWorker do worker.process_commit_message(project, commit, user, user, false) end + + it 'does not create cross references' do + expect(commit).to receive(:create_cross_references!).with(user, []) + + worker.process_commit_message(project, commit, user, user, false) + end end - it 'creates cross references' do - expect(commit).to receive(:create_cross_references!) + context 'when commit is a merge request merge commit to the default branch' do + let(:merge_request) do + create(:merge_request, + description: "Closes #{issue.to_reference}", + source_branch: 'feature-merged', + target_branch: 'master', + source_project: project) + end - worker.process_commit_message(project, commit, user, user) + let(:commit) do + project.repository.create_branch('feature-merged', 'feature') + + MergeRequests::MergeService + .new(project, merge_request.author) + .execute(merge_request) + + merge_request.reload.merge_commit + end + + it 'does not close any issues from the commit message' do + expect(worker).not_to receive(:close_issues) + + worker.process_commit_message(project, commit, user, user, true) + end + + it 'still creates cross references' do + expect(commit).to receive(:create_cross_references!).with(user, []) + + worker.process_commit_message(project, commit, user, user, true) + end end end |