diff --git a/.flayignore b/.flayignore index b63ce4c4df0..acac0ce14c9 100644 --- a/.flayignore +++ b/.flayignore @@ -5,3 +5,4 @@ app/policies/project_policy.rb app/models/concerns/relative_positioning.rb app/workers/stuck_merge_jobs_worker.rb lib/gitlab/redis/*.rb +lib/gitlab/gitaly_client/operation_service.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a31817e2b8d..15d9117976a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,6 +195,10 @@ entry. - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - [BUGIFX] Improves subgroup creation permissions. !13418 +## 9.5.7 (2017-10-03) + +- Fix gitlab rake:import:repos task. + ## 9.5.6 (2017-09-29) - [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242 diff --git a/Gemfile.lock b/Gemfile.lock index a0ad2716c01..4622fd141ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -910,7 +910,7 @@ GEM json (>= 1.8.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.2) + unf_ext (0.0.7.4) unicode-display_width (1.3.0) unicorn (5.1.0) kgio (~> 2.6) diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index af718e894cf..ce05b3eabec 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -1,30 +1,12 @@ import Jed from 'jed'; - import sprintf from './sprintf'; -/** - This is required to require all the translation folders in the current directory - this saves us having to do this manually & keep up to date with new languages -**/ -function requireAll(requireContext) { return requireContext.keys().map(requireContext); } - -const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/)); -const locales = allLocales.reduce((d, obj) => { - const data = d; - const localeKey = Object.keys(obj)[0]; - - data[localeKey] = obj[localeKey]; - - return data; -}, {}); - const langAttribute = document.querySelector('html').getAttribute('lang'); const lang = (langAttribute || 'en').replace(/-/g, '_'); -const locale = new Jed(locales[lang]); +const locale = new Jed(window.translations || {}); /** Translates `text` - @param text The text to be translated @returns {String} The translated text **/ diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 0db2abe507d..af0658eb668 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper'; $el.text(gl.text.addDelimiter(count)); }; + MergeRequest.prototype.hideCloseButton = function() { + const el = document.querySelector('.merge-request .issuable-actions'); + const closeDropdownItem = el.querySelector('li.close-item'); + if (closeDropdownItem) { + closeDropdownItem.classList.add('hidden'); + // Selects the next dropdown item + el.querySelector('li.report-item').click(); + } else { + // No dropdown just hide the Close button + el.querySelector('.btn-close').classList.add('hidden'); + } + // Dropdown for mobile screen + el.querySelector('li.js-close-item').classList.add('hidden'); + }; + return MergeRequest; })(); }).call(window); diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue index b8a16356576..b4067d229aa 100644 --- a/app/assets/javascripts/notebook/cells/code.vue +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -1,18 +1,3 @@ - - + + + ``` + +1. Properties in a Vue Component: 1. `name` 1. `props` 1. `mixins` @@ -490,6 +508,7 @@ On those a default key should not be provided. 1. `beforeDestroy` 1. `destroyed` + #### Vue and Bootstrap 1. Tooltips: Do not rely on `has-tooltip` class name for Vue components diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 2607353782a..277e0cd5f00 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -428,7 +428,7 @@ is a good example of this pattern. ## Style guide -Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs) +Please refer to the Vue section of our [style guide](style_guide_js.md#vue-js) for best practices while writing your Vue components and templates. ## Testing Vue Components diff --git a/doc/development/testing.md b/doc/development/testing.md index d856b003353..4d5b90de6fc 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -302,7 +302,7 @@ range of inputs, might look like this: ```ruby describe "#==" do - using Rspec::Parameterized::TableSyntax + using RSpec::Parameterized::TableSyntax let(:project1) { create(:project) } let(:project2) { create(:project) } diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index fac91935a45..597c98fbf6b 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -11,6 +11,7 @@ This page gathers all the resources for the topic **Authentication** within GitL - [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/) - **Integrations:** - [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth) + - [GitLab as OpenID Connect identity provider](../../integration/openid_connect_provider.md) ## GitLab administrators diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 5c615daf464..2c4dfcff4a6 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -17,25 +17,25 @@ have its own space to store its Docker images. You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. ---- - ## Enable the Container Registry for your project +NOTE: **Note:** +If you cannot find the Container Registry entry under your project's settings, +that means that it is not enabled in your GitLab instance. Ask your administrator +to enable it. + 1. First, ask your system administrator to enable GitLab Container Registry following the [administration documentation](../../administration/container_registry.md). If you are using GitLab.com, this is enabled by default so you can start using the Registry immediately. - -1. Go to your project's settings and enable the **Container Registry** feature - on your project. For new projects this might be enabled by default. For - existing projects (prior GitLab 8.8), you will have to explicitly enable it. - - ![Enable Container Registry](img/container_registry_enable.png) - +1. Go to your [project's General settings](settings/index.md#sharing-and-permissions) + and enable the **Container Registry** feature on your project. For new + projects this might be enabled by default. For existing projects + (prior GitLab 8.8), you will have to explicitly enable it. 1. Hit **Save changes** for the changes to take effect. You should now be able - to see the **Registry** link in the project menu. + to see the **Registry** link in the sidebar. - ![Container Registry tab](img/container_registry_tab.png) +![Container Registry](img/container_registry.png) ## Build and push images diff --git a/doc/user/project/img/container_registry.png b/doc/user/project/img/container_registry.png new file mode 100644 index 00000000000..abbaf838538 Binary files /dev/null and b/doc/user/project/img/container_registry.png differ diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png deleted file mode 100644 index d067a8be1ca..00000000000 Binary files a/doc/user/project/img/container_registry_enable.png and /dev/null differ diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png deleted file mode 100644 index a85237271d9..00000000000 Binary files a/doc/user/project/img/container_registry_tab.png and /dev/null differ diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png index cf7f519f783..5f6dc9e4e8b 100644 Binary files a/doc/user/project/img/issue_board.png and b/doc/user/project/img/issue_board.png differ diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png index c6b17ada40e..3666dbb87ab 100644 Binary files a/doc/user/project/img/issue_board_move_issue_card_list.png and b/doc/user/project/img/issue_board_move_issue_card_list.png differ diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png deleted file mode 100644 index badfbed0bbe..00000000000 Binary files a/doc/user/project/img/labels_assign_label_in_new_issue.png and /dev/null differ diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png index 474953d565b..7934e3bfb5e 100644 Binary files a/doc/user/project/img/labels_default.png and b/doc/user/project/img/labels_default.png differ diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png index 3aca77f0070..6a1ebfc2ecb 100644 Binary files a/doc/user/project/img/labels_filter.png and b/doc/user/project/img/labels_filter.png differ diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png index 5609a1f6d7f..419e555e709 100644 Binary files a/doc/user/project/img/labels_filter_by_priority.png and b/doc/user/project/img/labels_filter_by_priority.png differ diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png index b44b4bd296d..e26425d0188 100644 Binary files a/doc/user/project/img/labels_new_label.png and b/doc/user/project/img/labels_new_label.png differ diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png index 3e888f36364..d602a3c90ec 100644 Binary files a/doc/user/project/img/labels_prioritize.png and b/doc/user/project/img/labels_prioritize.png differ diff --git a/doc/user/project/img/project_repository_settings.png b/doc/user/project/img/project_repository_settings.png index 1aa7efc36f1..aa4d4452c87 100644 Binary files a/doc/user/project/img/project_repository_settings.png and b/doc/user/project/img/project_repository_settings.png differ diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index e2cc67726e0..96a5a23ee13 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -12,6 +12,8 @@ Other interesting links: - [GitLab Issue Board landing page on about.gitlab.com][landing] - [YouTube video introduction to Issue Boards][youtube] +![GitLab Issue Board](img/issue_board.png) + ## Overview The Issue Board builds on GitLab's existing @@ -89,10 +91,6 @@ two defaults: - **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left. - **Closed** (default): shows all closed issues. Always appears on the very right. -![GitLab Issue Board](img/issue_board.png) - ---- - In short, here's a list of actions you can take in an Issue Board: - [Create a new list](#creating-a-new-list). diff --git a/doc/user/project/issues/img/button_close_issue.png b/doc/user/project/issues/img/button_close_issue.png index 8fb2e23f58a..05d257ce9bf 100644 Binary files a/doc/user/project/issues/img/button_close_issue.png and b/doc/user/project/issues/img/button_close_issue.png differ diff --git a/doc/user/project/issues/img/group_issues_list_view.png b/doc/user/project/issues/img/group_issues_list_view.png index 5d20e8cbc89..bba964076d0 100644 Binary files a/doc/user/project/issues/img/group_issues_list_view.png and b/doc/user/project/issues/img/group_issues_list_view.png differ diff --git a/doc/user/project/issues/img/issue_board.png b/doc/user/project/issues/img/issue_board.png index 1759b28a9ef..87b1016cc76 100644 Binary files a/doc/user/project/issues/img/issue_board.png and b/doc/user/project/issues/img/issue_board.png differ diff --git a/doc/user/project/issues/img/issue_template.png b/doc/user/project/issues/img/issue_template.png index c63229a4af2..0e4c8df897b 100644 Binary files a/doc/user/project/issues/img/issue_template.png and b/doc/user/project/issues/img/issue_template.png differ diff --git a/doc/user/project/issues/img/issues_main_view.png b/doc/user/project/issues/img/issues_main_view.png index 4faa42e40ee..a929916c682 100644 Binary files a/doc/user/project/issues/img/issues_main_view.png and b/doc/user/project/issues/img/issues_main_view.png differ diff --git a/doc/user/project/issues/img/issues_main_view_numbered.jpg b/doc/user/project/issues/img/issues_main_view_numbered.jpg index 4b5d7fba459..b4b68476d24 100644 Binary files a/doc/user/project/issues/img/issues_main_view_numbered.jpg and b/doc/user/project/issues/img/issues_main_view_numbered.jpg differ diff --git a/doc/user/project/issues/img/new_issue.png b/doc/user/project/issues/img/new_issue.png index e72ac49d6b9..07d65a93070 100644 Binary files a/doc/user/project/issues/img/new_issue.png and b/doc/user/project/issues/img/new_issue.png differ diff --git a/doc/user/project/issues/img/new_issue_from_issue_board.png b/doc/user/project/issues/img/new_issue_from_issue_board.png index 9c2b3ff50fa..da892eff0a6 100644 Binary files a/doc/user/project/issues/img/new_issue_from_issue_board.png and b/doc/user/project/issues/img/new_issue_from_issue_board.png differ diff --git a/doc/user/project/issues/img/new_issue_from_open_issue.png b/doc/user/project/issues/img/new_issue_from_open_issue.png index 2aed5372830..c6f3f0617ab 100644 Binary files a/doc/user/project/issues/img/new_issue_from_open_issue.png and b/doc/user/project/issues/img/new_issue_from_open_issue.png differ diff --git a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png index cddf36b7457..4b9535f6b15 100644 Binary files a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png and b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png differ diff --git a/doc/user/project/issues/img/new_issue_from_tracker_list.png b/doc/user/project/issues/img/new_issue_from_tracker_list.png index 7e5413f0b7d..66793cb44fa 100644 Binary files a/doc/user/project/issues/img/new_issue_from_tracker_list.png and b/doc/user/project/issues/img/new_issue_from_tracker_list.png differ diff --git a/doc/user/project/issues/img/project_issues_list_view.png b/doc/user/project/issues/img/project_issues_list_view.png index 2fcc9e8d9da..584a81aab8a 100644 Binary files a/doc/user/project/issues/img/project_issues_list_view.png and b/doc/user/project/issues/img/project_issues_list_view.png differ diff --git a/doc/user/project/issues/img/sidebar_move_issue.png b/doc/user/project/issues/img/sidebar_move_issue.png index 111f7861364..1e688cec894 100644 Binary files a/doc/user/project/issues/img/sidebar_move_issue.png and b/doc/user/project/issues/img/sidebar_move_issue.png differ diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index 8ec7adad172..21a2e1213ec 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -20,8 +20,6 @@ Head over a single project and navigate to **Issues > Labels**. The first time you visit this page, you'll notice that there are no labels created yet. -![Generate new labels](img/labels_generate.png) - Creating a new label from scratch is as easy as pressing the **New label** button. From there on you can choose the name, give it an optional description, a color and you are set. @@ -32,21 +30,23 @@ When you are ready press the **Create label** button to create the new label. --- -## Default Labels +## Default labels -It's possible to populate the labels for your project from a set of predefined labels. - -### Generate GitLab's predefined label set - -![Generate new labels](img/labels_generate.png) +The very first time you visit the labels area, it's gonna be empty. In that +case, it's possible to populate the labels for your project from a set of +predefined labels. Click the link to 'Generate a default set of labels' and GitLab will -generate a set of predefined labels for you. There are 8 default generated labels -in total and you can see them in the screenshot below. +generate them for you. There are 8 default generated labels in total: -![Default generated labels](img/labels_default.png) - ---- +- bug +- confirmed +- critical +- discussion +- documentation +- enhancement +- suggestion +- support ## Labels Overview @@ -102,30 +102,25 @@ If you work on a large or popular project, try subscribing only to the labels that are relevant to you. You’ll notice it’ll be much easier to focus on what’s important. -## Create a new label right from the issue tracker +## Create a new label when inside an issue -> Introduced in GitLab 8.6. - -There are times when you are already in the issue tracker searching for a +There are times when you are already inside an issue searching to assign a label, only to realize it doesn't exist. Instead of going to the **Labels** page and being distracted from your original purpose, you can create new labels on the fly. -Select **Create new** from the labels dropdown list, provide a name, pick a -color and hit **Create**. +Expand the issue sidebar and select **Create new label** from the labels dropdown +list. Provide a name, pick a color and hit **Create**. The new label will be +ready to used right away! -![Create new label on the fly](img/labels_new_label_on_the_fly_create.png) ![New label on the fly](img/labels_new_label_on_the_fly.png) ## Assigning labels to issues and merge requests There are generally two ways to assign a label to an issue or merge request. -You can assign a label when you first create or edit an issue or merge request. - -![Assign label in new issue](img/labels_assign_label_in_new_issue.png) - ---- +The first one is to assign a label when you first create or edit an issue or +merge request. The second way is by using the right sidebar when inside an issue or merge request. Expand it and hit **Edit** in the labels area. Start typing the name diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md index 64b94d81024..22ef11e4049 100644 --- a/doc/user/project/merge_requests/cherry_pick_changes.md +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -2,24 +2,19 @@ > [Introduced][ce-3514] in GitLab 8.7. ---- - GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick] -with introducing a **Cherry-pick** button in Merge Requests and commit details. +with introducing a **Cherry-pick** button in merge requests and commit details. -## Cherry-picking a Merge Request +## Cherry-picking a merge request -After the Merge Request has been merged, a **Cherry-pick** button will be available -to cherry-pick the changes introduced by that Merge Request: +After the merge request has been merged, a **Cherry-pick** button will be available +to cherry-pick the changes introduced by that merge request. ![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png) ---- - -You can cherry-pick the changes directly into the selected branch or you can opt to -create a new Merge Request with the cherry-pick changes: - -![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png) +After you click that button, a modal will appear where you can choose to +cherry-pick the changes directly into the selected branch or you can opt to +create a new merge request with the cherry-pick changes ## Cherry-picking a Commit @@ -27,15 +22,9 @@ You can cherry-pick a Commit from the Commit details page: ![Cherry-pick commit](img/cherry_pick_changes_commit.png) ---- - -Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes -directly into the target branch or create a new Merge Request to cherry-pick the -changes: - -![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png) - ---- +Similar to cherry-picking a merge request, you can opt to cherry-pick the changes +directly into the target branch or create a new merge request to cherry-pick the +changes. Please note that when cherry-picking merge commits, the mainline will always be the first parent. If you want to use a different mainline then you need to do that diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png index 5ab094ab367..7dc344f8cf6 100644 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png and b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png differ diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png deleted file mode 100644 index 42dcb9203ec..00000000000 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png and /dev/null differ diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png index 71227747182..811b0998f85 100644 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png and b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png differ diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png deleted file mode 100644 index 604eb22f51c..00000000000 Binary files a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png and /dev/null differ diff --git a/doc/user/project/merge_requests/img/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png deleted file mode 100644 index e612a39716e..00000000000 Binary files a/doc/user/project/merge_requests/img/commit_compare.png and /dev/null differ diff --git a/doc/user/project/merge_requests/img/merge_request.png b/doc/user/project/merge_requests/img/merge_request.png new file mode 100644 index 00000000000..f9ca6348953 Binary files /dev/null and b/doc/user/project/merge_requests/img/merge_request.png differ diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png index 33f5a4a7a02..d7f0535d3c5 100644 Binary files a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png and b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png differ diff --git a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png deleted file mode 100644 index ef7b6dae553..00000000000 Binary files a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png and /dev/null differ diff --git a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png deleted file mode 100644 index f6540c9dd33..00000000000 Binary files a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png and /dev/null differ diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png index 33c58d2abff..3883fb4bc1c 100644 Binary files a/doc/user/project/merge_requests/img/versions.png and b/doc/user/project/merge_requests/img/versions.png differ diff --git a/doc/user/project/merge_requests/img/versions_compare.png b/doc/user/project/merge_requests/img/versions_compare.png index db978ea7b1d..f5bd85dc7c1 100644 Binary files a/doc/user/project/merge_requests/img/versions_compare.png and b/doc/user/project/merge_requests/img/versions_compare.png differ diff --git a/doc/user/project/merge_requests/img/versions_dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png index 889a2d93e6c..cc70a5bf14b 100644 Binary files a/doc/user/project/merge_requests/img/versions_dropdown.png and b/doc/user/project/merge_requests/img/versions_dropdown.png differ diff --git a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png index 047b0b4620f..0c492aca363 100644 Binary files a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png and b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png differ diff --git a/doc/user/project/merge_requests/img/wip_mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png index 8bd206bc24a..e405879b28a 100644 Binary files a/doc/user/project/merge_requests/img/wip_mark_as_wip.png and b/doc/user/project/merge_requests/img/wip_mark_as_wip.png differ diff --git a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png index c0bfa6a35a2..d7f8c419945 100644 Binary files a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png and b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png differ diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 8e081b4f0b8..6289fcf3c2b 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -3,6 +3,8 @@ Merge requests allow you to exchange changes you made to source code and collaborate with other people on the same project. +![Merge request view](img/merge_request.png) + ## Overview A Merge Request (**MR**) is the basis of GitLab as a code collaboration diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md index 5ead9f4177f..8cf8a59dbfe 100644 --- a/doc/user/project/merge_requests/revert_changes.md +++ b/doc/user/project/merge_requests/revert_changes.md @@ -2,51 +2,39 @@ > [Introduced][ce-1990] in GitLab 8.5. ---- - GitLab implements Git's powerful feature to [revert any commit][git-revert] -with introducing a **Revert** button in Merge Requests and commit details. +with introducing a **Revert** button in merge requests and commit details. ## Reverting a Merge Request -_**Note:** The **Revert** button will only be available for Merge Requests -created since GitLab 8.5. However, you can still revert a Merge Request -by reverting the merge commit from the list of Commits page._ +NOTE: **Note:** +The **Revert** button will only be available for merge requests +created since GitLab 8.5. However, you can still revert a merge request +by reverting the merge commit from the list of Commits page. After the Merge Request has been merged, a **Revert** button will be available -to revert the changes introduced by that Merge Request: +to revert the changes introduced by that merge request. -![Revert Merge Request](img/revert_changes_mr.png) +![Revert Merge Request](img/cherry_pick_changes_mr.png) ---- +After you click that button, a modal will appear where you can choose to +revert the changes directly into the selected branch or you can opt to +create a new merge request with the revert changes. -You can revert the changes directly into the selected branch or you can opt to -create a new Merge Request with the revert changes: - -![Revert Merge Request modal](img/revert_changes_mr_modal.png) - ---- - -After the Merge Request has been reverted, the **Revert** button will not be +After the merge request has been reverted, the **Revert** button will not be available anymore. ## Reverting a Commit You can revert a Commit from the Commit details page: -![Revert commit](img/revert_changes_commit.png) +![Revert commit](img/cherry_pick_changes_commit.png) ---- +Similar to reverting a merge request, you can opt to revert the changes +directly into the target branch or create a new merge request to revert the +changes. -Similar to reverting a Merge Request, you can opt to revert the changes -directly into the target branch or create a new Merge Request to revert the -changes: - -![Revert commit modal](img/revert_changes_commit_modal.png) - ---- - -After the Commit has been reverted, the **Revert** button will not be available +After the commit has been reverted, the **Revert** button will not be available anymore. Please note that when reverting merge commits, the mainline will always be the diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index f88738b4c61..3490bbd968c 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -26,7 +26,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I fork to my namespace' do - page.within '.fork-namespaces' do + page.within '.fork-thumbnail-container' do click_link current_user.name end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 22b735c6f7b..89b654253cb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -53,14 +53,15 @@ module Gitlab # Rugged repo object attr_reader :rugged - attr_reader :storage, :gl_repository, :relative_path + attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver - # 'path' must be the path to a _bare_ git repository, e.g. - # /path/to/my-repo.git + # This initializer method is only used on the client side (gitlab-ce). + # Gitaly-ruby uses a different initializer. def initialize(storage, relative_path, gl_repository) @storage = storage @relative_path = relative_path @gl_repository = gl_repository + @gitaly_resolver = Gitlab::GitalyClient storage_path = Gitlab.config.repositories.storages[@storage]['path'] @path = File.join(storage_path, @relative_path) @@ -676,7 +677,13 @@ module Gitlab end def rm_branch(branch_name, user:) - OperationService.new(user, self).rm_branch(find_branch(branch_name)) + gitaly_migrate(:operation_user_delete_branch) do |is_enabled| + if is_enabled + gitaly_operations_client.user_delete_branch(branch_name, user) + else + OperationService.new(user, self).rm_branch(find_branch(branch_name)) + end + end end def rm_tag(tag_name, user:) @@ -981,9 +988,9 @@ module Gitlab def with_repo_tmp_commit(start_repository, start_branch_name, sha) tmp_ref = fetch_ref( - start_repository.path, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - "refs/tmp/#{SecureRandom.hex}/head" + start_repository, + source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", + target_ref: "refs/tmp/#{SecureRandom.hex}/head" ) yield commit(sha) @@ -1015,13 +1022,27 @@ module Gitlab end end - def write_ref(ref_path, sha) - rugged.references.create(ref_path, sha, force: true) + def write_ref(ref_path, ref) + raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') + raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") + + command = [Gitlab.config.git.bin_path] + %w[update-ref --stdin -z] + input = "update #{ref_path}\x00#{ref}\x00\x00" + output, status = circuit_breaker.perform do + popen(command, path) { |stdin| stdin.write(input) } + end + + raise GitError, output unless status.zero? end - def fetch_ref(source_path, source_ref, target_ref) - args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - message, status = run_git(args) + def fetch_ref(source_repository, source_ref:, target_ref:) + message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled| + if is_enabled + gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref) + else + local_fetch_ref(source_repository.path, source_ref: source_ref, target_ref: target_ref) + end + end # Make sure ref was created, and raise Rugged::ReferenceError when not raise Rugged::ReferenceError, message if status != 0 @@ -1030,9 +1051,9 @@ module Gitlab end # Refactoring aid; allows us to copy code from app/models/repository.rb - def run_git(args) + def run_git(args, env: {}) circuit_breaker.perform do - popen([Gitlab.config.git.bin_path, *args], path) + popen([Gitlab.config.git.bin_path, *args], path, env) end end @@ -1489,9 +1510,33 @@ module Gitlab OperationService.new(user, self).add_branch(branch_name, target_object.oid) find_branch(branch_name) - rescue Rugged::ReferenceError + rescue Rugged::ReferenceError => ex raise InvalidRef, ex end + + def local_fetch_ref(source_path, source_ref:, target_ref:) + args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) + run_git(args) + end + + def gitaly_fetch_ref(source_repository, source_ref:, target_ref:) + gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh')) + gitaly_address = gitaly_resolver.address(source_repository.storage) + gitaly_token = gitaly_resolver.token(source_repository.storage) + + request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository) + env = { + 'GITALY_ADDRESS' => gitaly_address, + 'GITALY_PAYLOAD' => request.to_json, + 'GITALY_WD' => Dir.pwd, + 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack" + } + env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present? + + args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref}) + + run_git(args, env: env) + end end end end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index e0943d3a3eb..92a6a672534 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -31,7 +31,7 @@ module Gitlab output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys) unless status.zero? - raise "Got a non-zero exit code while calling out `#{args.join(' ')}`." + raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}" end output.split("\n") diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 46bd5c18603..81ddaf13e10 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -60,6 +60,20 @@ module Gitlab target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit) end + + def user_delete_branch(branch_name, user) + request = Gitaly::UserDeleteBranchRequest.new( + repository: @gitaly_repo, + branch_name: GitalyClient.encode(branch_name), + user: Util.gitaly_user(user) + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request) + + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + end + end end end end diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb index 104280f520a..d7d24eeb37b 100644 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -25,7 +25,7 @@ module Gitlab Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ "#{MAX_RSS}" - Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"\ + Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\ "in #{GRACE_TIME} seconds" sleep(GRACE_TIME) diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index 222021e8802..f30c771837a 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -12,8 +12,9 @@ module Gitlab # # Project.where("id IN (#{sql})") class Union - def initialize(relations) + def initialize(relations, remove_duplicates: true) @relations = relations + @remove_duplicates = remove_duplicates end def to_sql @@ -25,7 +26,11 @@ module Gitlab @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) end - fragments.join("\nUNION\n") + fragments.join("\n#{union_keyword}\n") + end + + def union_keyword + @remove_duplicates ? 'UNION' : 'UNION ALL' end end end diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 259a755d724..a42f02a84fd 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -3,8 +3,8 @@ namespace :gitlab do desc 'GitLab | Assets | Compile all frontend assets' task compile: [ 'yarn:check', - 'rake:assets:precompile', 'gettext:po_to_json', + 'rake:assets:precompile', 'webpack:compile', 'fix_urls' ] diff --git a/qa/Gemfile b/qa/Gemfile index 5d089a45934..ff29824529f 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,5 +1,6 @@ source 'https://rubygems.org' +gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'capybara', '~> 2.12.1' gem 'capybara-screenshot', '~> 1.0.14' gem 'rake', '~> 12.0.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 4dd71aa5010..95aeef10752 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -3,6 +3,7 @@ GEM specs: addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) + byebug (9.0.6) capybara (2.12.1) addressable mime-types (>= 1.16) @@ -13,22 +14,27 @@ GEM capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy - capybara-webkit (1.12.0) - capybara (>= 2.3.0, < 2.13.0) - json childprocess (0.7.0) ffi (~> 1.0, >= 1.0.11) + coderay (1.1.1) diff-lcs (1.3) ffi (1.9.18) - json (2.0.3) launchy (2.4.3) addressable (~> 2.3) + method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_portile2 (2.1.0) nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) + pry (0.10.4) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (3.4.2) + byebug (~> 9.0) + pry (~> 0.10) public_suffix (2.0.5) rack (2.0.1) rack-test (0.6.3) @@ -52,6 +58,7 @@ GEM childprocess (~> 0.5) rubyzip (~> 1.0) websocket (~> 1.0) + slop (3.6.0) websocket (1.2.4) xpath (2.0.0) nokogiri (~> 1.3) @@ -62,7 +69,7 @@ PLATFORMS DEPENDENCIES capybara (~> 2.12.1) capybara-screenshot (~> 1.0.14) - capybara-webkit (~> 1.12.0) + pry-byebug (~> 3.4.1) rake (~> 12.0.0) rspec (~> 3.5) selenium-webdriver (~> 2.53) diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index f4619042e34..baa06b1c75e 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -4,8 +4,6 @@ module QA class Menu < Page::Base def go_to_license link = find_link 'License' - # Click space to scroll this link into the view - link.send_keys(:space) link.click end end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index 4dfdd6cd93c..79c681168cc 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -43,8 +43,7 @@ module QA Capybara.register_driver :chrome do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { - 'binary' => '/usr/bin/google-chrome-stable', - 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024] + 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] } ) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 629c131aee6..e46d1995498 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -96,18 +96,6 @@ describe Projects::MergeRequestsController do expect(response).to match_response_schema('entities/merge_request') end end - - context 'number of queries', :request_store do - it 'verifies number of queries' do - # pre-create objects - merge_request - - recorded = ActiveRecord::QueryRecorder.new { go(format: :json) } - - expect(recorded.count).to be_within(5).of(30) - expect(recorded.cached_count).to eq(0) - end - end end describe "as diff" do diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb index 2805968dcd9..5d9d5351687 100644 --- a/spec/controllers/projects/registry/repositories_controller_spec.rb +++ b/spec/controllers/projects/registry/repositories_controller_spec.rb @@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do expect { go_to_index }.to change { ContainerRepository.all.count }.by(1) expect(ContainerRepository.first).to be_root_repository end + + it 'json has a list of projects' do + go_to_index(format: :json) + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/repositories') + end end context 'when there are no tags for this repository' do @@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do it 'does not ensure root container repository' do expect { go_to_index }.not_to change { ContainerRepository.all.count } end + + it 'responds with json if asked' do + go_to_index(format: :json) + + expect(response).to have_http_status(:ok) + expect(json_response).to be_kind_of(Array) + end + end + end + end + + describe 'DELETE destroy' do + context 'when root container repository exists' do + let!(:repository) do + create(:container_repository, :root, project: project) + end + + before do + stub_container_registry_tags(repository: :any, tags: []) + end + + it 'deletes a repository' do + expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1) + + expect(response).to have_http_status(:no_content) end end end @@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do end end - def go_to_index + def go_to_index(format: :html) get :index, namespace_id: project.namespace, - project_id: project + project_id: project, + format: format + end + + def delete_repository(repository) + delete :destroy, namespace_id: project.namespace, + project_id: project, + id: repository, + format: :json end end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index f4af3587d23..bb702ebeb23 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do let(:user) { create(:user) } let(:project) { create(:project, :private) } + let(:repository) do + create(:container_repository, name: 'image', project: project) + end + before do sign_in(user) stub_container_registry_config(enabled: true) end - context 'when user has access to registry' do - before do - project.add_developer(user) + describe 'GET index' do + let(:tags) do + Array.new(40) { |i| "tag#{i}" } end - describe 'POST destroy' do + before do + stub_container_registry_tags(repository: /image/, tags: tags) + end + + context 'when user can control the registry' do + before do + project.add_developer(user) + end + + it 'receive a list of tags' do + get_tags + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/tags') + expect(response).to include_pagination_headers + end + end + + context 'when user can read the registry' do + before do + project.add_reporter(user) + end + + it 'receive a list of tags' do + get_tags + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/tags') + expect(response).to include_pagination_headers + end + end + + context 'when user does not have access to registry' do + before do + project.add_guest(user) + end + + it 'does not receive a list of tags' do + get_tags + + expect(response).to have_http_status(:not_found) + end + end + + private + + def get_tags + get :index, namespace_id: project.namespace, + project_id: project, + repository_id: repository, + format: :json + end + end + + describe 'POST destroy' do + context 'when user has access to registry' do + before do + project.add_developer(user) + end + context 'when there is matching tag present' do before do - stub_container_registry_tags(repository: /image/, tags: %w[rc1 test.]) - end - - let(:repository) do - create(:container_repository, name: 'image', project: project) + stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.]) end it 'makes it possible to delete regular tag' do @@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do end end end - end - def destroy_tag(name) - post :destroy, namespace_id: project.namespace, - project_id: project, - repository_id: repository, - id: name + private + + def destroy_tag(name) + post :destroy, namespace_id: project.namespace, + project_id: project, + repository_id: repository, + id: name, + format: :json + end end end diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index e5abfd67d60..0dd1238d6e2 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -12,7 +12,7 @@ FactoryGirl.define do deployment.project ||= deployment.environment.project unless deployment.project.repository_exists? - allow(deployment.project.repository).to receive(:fetch_ref) + allow(deployment.project.repository).to receive(:create_ref) end end end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index ae39ba4da6b..45213dc6995 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Container Registry" do +describe "Container Registry", js: true do let(:user) { create(:user) } let(:project) { create(:project) } @@ -41,16 +41,19 @@ describe "Container Registry" do expect_any_instance_of(ContainerRepository) .to receive(:delete_tags!).and_return(true) - click_on 'Remove repository' + click_on(class: 'js-remove-repo') end scenario 'user removes a specific tag from container repository' do visit_container_registry + find('.js-toggle-repo').trigger('click') + wait_for_requests + expect_any_instance_of(ContainerRegistry::Tag) .to receive(:delete).and_return(true) - click_on 'Remove tag' + click_on(class: 'js-delete-registry') end end diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index ad06cee4e81..2f407b13c2f 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) + visit project_branches_path(project, 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 ad4527a0b74..d1f5623554d 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -5,12 +5,6 @@ describe 'Branches' do let(:project) { create(:project, :public, :repository) } let(:repository) { project.repository } - def set_protected_branch_name(branch_name) - find(".js-protected-branch-select").click - find(".dropdown-input-field").set(branch_name) - click_on("Create wildcard #{branch_name}") - end - context 'logged in as developer' do before do sign_in(user) @@ -18,12 +12,10 @@ describe 'Branches' do end describe 'Initial branches page' do - it 'shows all the branches' do + it 'shows all the branches sorted by last updated by default' do visit project_branches_path(project) - repository.branches_sorted_by(:name).first(20).each do |branch| - expect(page).to have_content("#{branch.name}") - end + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc)) end it 'sorts the branches by name' do @@ -32,22 +24,7 @@ describe 'Branches' do click_button "Last updated" # Open sorting dropdown click_link "Name" - sorted = repository.branches_sorted_by(:name).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) - end - - it 'sorts the branches by last updated' do - visit project_branches_path(project) - - click_button "Last updated" # Open sorting dropdown - click_link "Last updated" - - sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name)) end it 'sorts the branches by oldest updated' do @@ -56,10 +33,7 @@ describe 'Branches' do click_button "Last updated" # Open sorting dropdown click_link "Oldest updated" - sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc)) end it 'avoids a N+1 query in branches index' do @@ -99,28 +73,6 @@ describe 'Branches' do expect(find('.all-branches')).to have_selector('li', count: 0) end end - - describe 'Delete protected branch' do - before do - project.add_user(user, :master) - visit project_protected_branches_path(project) - set_protected_branch_name('fix') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('fix') } - expect(ProtectedBranch.count).to eq(1) - project.add_user(user, :developer) - end - - it 'does not allow devleoper to removes protected branch', js: true do - visit project_branches_path(project) - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_css('.btn-remove.disabled') - end - end end context 'logged in as master' do @@ -136,37 +88,6 @@ describe 'Branches' do expect(page).to have_content("Protected branches can be managed in project settings") end end - - describe 'Delete protected branch' do - before do - visit project_protected_branches_path(project) - set_protected_branch_name('fix') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('fix') } - expect(ProtectedBranch.count).to eq(1) - end - - it 'removes branch after modal confirmation', js: true 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) - page.find('[data-target="#modal-delete-branch"]').trigger(:click) - - expect(page).to have_css('.js-delete-branch[disabled]') - fill_in 'delete_branch_input', with: 'fix' - click_link 'Delete protected branch' - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('No branches to show') - end - end end context 'logged out' do @@ -180,4 +101,13 @@ describe 'Branches' do end end end + + def sorted_branches(repository, count:, sort_by:) + sorted_branches = + repository.branches_sorted_by(sort_by).first(count).map do |branch| + Regexp.escape(branch.name) + end + + Regexp.new(sorted_branches.join('.*')) + end end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 3677bf38724..bf9885f73bd 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,11 +1,148 @@ require 'spec_helper' -feature 'Protected Branches', js: true do - let(:user) { create(:user, :admin) } +feature 'Protected Branches', :js do + let(:user) { create(:user) } + let(:admin) { create(:admin) } let(:project) { create(:project, :repository) } - before do - sign_in(user) + context 'logged in as developer' do + before do + project.add_developer(user) + sign_in(user) + end + + describe 'Delete protected branch' do + before do + create(:protected_branch, project: project, name: 'fix') + expect(ProtectedBranch.count).to eq(1) + end + + it 'does not allow developer to removes protected branch' do + visit project_branches_path(project) + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_css('.btn-remove.disabled') + end + end + end + + context 'logged in as master' do + before do + project.add_master(user) + sign_in(user) + end + + describe 'Delete protected branch' do + before do + create(:protected_branch, project: project, name: 'fix') + expect(ProtectedBranch.count).to eq(1) + end + + it 'removes branch after modal confirmation' 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) + page.find('[data-target="#modal-delete-branch"]').trigger(:click) + + expect(page).to have_css('.js-delete-branch[disabled]') + fill_in 'delete_branch_input', with: 'fix' + click_link 'Delete protected branch' + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('No branches to show') + end + end + end + + context 'logged in as admin' do + before do + sign_in(admin) + end + + describe "explicit protected branches" do + it "allows creating explicit protected branches" do + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('some-branch') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('some-branch') + end + + it "displays the last commit on the matching branch if it exists" do + commit = create(:commit, project: project) + project.repository.add_branch(admin, 'some-branch', commit.id) + + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } + end + + it "displays an error message if the named branch does not exist" do + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + end + end + + describe "wildcard protected branches" do + it "allows creating protected branches with a wildcard" do + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('*-stable') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('*-stable') + end + + it "displays the number of matching branches" do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') + + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } + end + + it "displays all the branches matching the wildcard" do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') + project.repository.add_branch(admin, 'development', 'master') + + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" + + visit project_protected_branches_path(project) + click_on "2 matching branches" + + within(".protected-branches-list") do + expect(page).to have_content("production-stable") + expect(page).to have_content("staging-stable") + expect(page).not_to have_content("development") + end + end + end + + describe "access control" do + include_examples "protected branches > access control > CE" + end end def set_protected_branch_name(branch_name) @@ -13,81 +150,4 @@ feature 'Protected Branches', js: true do find(".dropdown-input-field").set(branch_name) click_on("Create wildcard #{branch_name}") end - - describe "explicit protected branches" do - it "allows creating explicit protected branches" do - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('some-branch') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('some-branch') - end - - it "displays the last commit on the matching branch if it exists" do - commit = create(:commit, project: project) - project.repository.add_branch(user, 'some-branch', commit.id) - - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } - end - - it "displays an error message if the named branch does not exist" do - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('branch was removed') } - end - end - - describe "wildcard protected branches" do - it "allows creating protected branches with a wildcard" do - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('*-stable') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('*-stable') - end - - it "displays the number of matching branches" do - project.repository.add_branch(user, 'production-stable', 'master') - project.repository.add_branch(user, 'staging-stable', 'master') - - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } - end - - it "displays all the branches matching the wildcard" do - project.repository.add_branch(user, 'production-stable', 'master') - project.repository.add_branch(user, 'staging-stable', 'master') - project.repository.add_branch(user, 'development', 'master') - - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" - - visit project_protected_branches_path(project) - click_on "2 matching branches" - - within(".protected-branches-list") do - expect(page).to have_content("production-stable") - expect(page).to have_content("staging-stable") - expect(page).not_to have_content("development") - end - end - end - - describe "access control" do - include_examples "protected branches > access control > CE" - end end diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 0796d9b8af9..30b4e56bc98 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -93,7 +93,7 @@ "merge_commit_message_with_description": { "type": "string" }, "diverged_commits_count": { "type": "integer" }, "commit_change_content_path": { "type": "string" }, - "remove_wip_path": { "type": "string" }, + "remove_wip_path": { "type": ["string", "null"] }, "commits_count": { "type": "integer" }, "remove_source_branch": { "type": ["boolean", "null"] }, "merge_ongoing": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/registry/repositories.json b/spec/fixtures/api/schemas/registry/repositories.json new file mode 100644 index 00000000000..4978bd89cda --- /dev/null +++ b/spec/fixtures/api/schemas/registry/repositories.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "repository.json" + } +} diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json new file mode 100644 index 00000000000..4175642eb00 --- /dev/null +++ b/spec/fixtures/api/schemas/registry/repository.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "required" : [ + "id", + "path", + "location", + "tags_path" + ], + "properties" : { + "id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "location": { + "type": "string" + }, + "tags_path": { + "type": "string" + }, + "destroy_path": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json new file mode 100644 index 00000000000..5bc307e0e64 --- /dev/null +++ b/spec/fixtures/api/schemas/registry/tag.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "required" : [ + "name", + "location" + ], + "properties" : { + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "total_size": { + "type": "integer" + }, + "created_at": { + "type": "date" + }, + "destroy_path": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/registry/tags.json b/spec/fixtures/api/schemas/registry/tags.json new file mode 100644 index 00000000000..c72f957459a --- /dev/null +++ b/spec/fixtures/api/schemas/registry/tags.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "tag.json" + } +} diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 4bc2205e642..3fd16d76f51 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -41,6 +41,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont remove_repository(project) end + it 'merge_requests/merge_request_of_current_user.html.raw' do |example| + merge_request.update(author: admin) + + render_merge_request(example.description, merge_request) + end + it 'merge_requests/merge_request_with_task_list.html.raw' do |example| create(:ci_build, :pending, pipeline: pipeline) diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/helpers/vuex_action_helper.js similarity index 100% rename from spec/javascripts/notes/stores/helpers.js rename to spec/javascripts/helpers/vuex_action_helper.js diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 6ff42e2378d..3ab901da6b6 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper'; expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); }); }); + + describe('hideCloseButton', () => { + describe('merge request of another user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the dropdown close item and selects the next item', () => { + const closeItem = this.el.querySelector('li.close-item'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + const reportItem = this.el.querySelector('li.report-item'); + + expect(closeItem).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + expect(reportItem).toHaveClass('droplab-item-selected'); + expect(reportItem).not.toHaveClass('hidden'); + }); + }); + + describe('merge request of current_user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the close button', () => { + const closeButton = this.el.querySelector('.btn-close'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + + expect(closeButton).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + }); + }); + }); }); }).call(window); diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index 1c8b1b98242..3f659af5c3b 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -33,6 +33,30 @@ describe('issue_comment_form component', () => { expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); }); + describe('handleSave', () => { + it('should request to save note when note is entered', () => { + vm.note = 'hello world'; + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + spyOn(vm, 'resizeTextarea'); + spyOn(vm, 'stopPolling'); + + vm.handleSave(); + expect(vm.isSubmitting).toEqual(true); + expect(vm.note).toEqual(''); + expect(vm.saveNote).toHaveBeenCalled(); + expect(vm.stopPolling).toHaveBeenCalled(); + expect(vm.resizeTextarea).toHaveBeenCalled(); + }); + + it('should toggle issue state when no note', () => { + spyOn(vm, 'toggleIssueState'); + + vm.handleSave(); + + expect(vm.toggleIssueState).toHaveBeenCalled(); + }); + }); + describe('textarea', () => { it('should render textarea with placeholder', () => { expect( @@ -40,6 +64,22 @@ describe('issue_comment_form component', () => { ).toEqual('Write a comment or drag your files here...'); }); + it('should make textarea disabled while requesting', (done) => { + const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button')); + vm.note = 'hello world'; + spyOn(vm, 'stopPolling'); + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + + vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton. + $submitButton.trigger('click'); + + vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea. + expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); + done(); + }); + }); + }); + it('should support quick actions', () => { expect( vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 2b2219dcf0c..3d1ca870ca4 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,5 +1,5 @@ import * as actions from '~/notes/stores/actions'; -import testAction from './helpers'; +import testAction from '../../helpers/vuex_action_helper'; import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; describe('Actions Notes Store', () => { diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js new file mode 100644 index 00000000000..43e7d9e1224 --- /dev/null +++ b/spec/javascripts/registry/components/app_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import registry from '~/registry/components/app.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { reposServerResponse } from '../mock_data'; + +describe('Registry List', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(registry); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a list of repos', (done) => { + setTimeout(() => { + expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('.container-image').length, + ).toEqual(reposServerResponse.length); + done(); + }); + }, 0); + }); + + describe('delete repository', () => { + it('should be possible to delete a repo', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined(); + done(); + }); + }, 0); + }); + }); + + describe('toggle repository', () => { + it('should open the container', (done) => { + setTimeout(() => { + Vue.nextTick(() => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual('fa fa-chevron-up'); + done(); + }); + }); + }, 0); + }); + }); + }); + + describe('without data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render empty message', (done) => { + setTimeout(() => { + expect( + vm.$el.querySelector('p').textContent.trim(), + ).toEqual('No container images stored for this project. Add one by following the instructions above.'); + done(); + }, 0); + }); + }); + + describe('while loading data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + vm = mountComponent(Component, { endpoint: 'foo' }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('should render a loading spinner', (done) => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js new file mode 100644 index 00000000000..5891921318a --- /dev/null +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import collapsibleComponent from '~/registry/components/collapsible_container.vue'; +import store from '~/registry/stores'; +import { repoPropsData } from '../mock_data'; + +describe('collapsible registry container', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(collapsibleComponent); + vm = new Component({ + store, + propsData: { + repo: repoPropsData, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('toggle', () => { + it('should be closed by default', () => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right'); + }); + + it('should be open when user clicks on closed repo', (done) => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBeDefined(); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-up'); + done(); + }); + }); + + it('should be closed when the user clicks on an opened repo', (done) => { + vm.$el.querySelector('.js-toggle-repo').click(); + + Vue.nextTick(() => { + vm.$el.querySelector('.js-toggle-repo').click(); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.container-image-tags')).toBe(null); + expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right'); + done(); + }); + }); + }); + }); + + describe('delete repo', () => { + it('should be possible to delete a repo', () => { + expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js new file mode 100644 index 00000000000..6aa61afc445 --- /dev/null +++ b/spec/javascripts/registry/components/table_registry_spec.js @@ -0,0 +1,49 @@ +import Vue from 'vue'; +import tableRegistry from '~/registry/components/table_registry.vue'; +import store from '~/registry/stores'; +import { repoPropsData } from '../mock_data'; + +describe('table registry', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(tableRegistry); + vm = new Component({ + store, + propsData: { + repo: repoPropsData, + }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a table with the registry list', () => { + expect( + vm.$el.querySelectorAll('table tbody tr').length, + ).toEqual(repoPropsData.list.length); + }); + + it('should render registry tag', () => { + const textRendered = vm.$el.querySelector('.table tbody tr').textContent.trim().replace(/\s\s+/g, ' '); + expect(textRendered).toContain(repoPropsData.list[0].tag); + expect(textRendered).toContain(repoPropsData.list[0].shortRevision); + expect(textRendered).toContain(repoPropsData.list[0].layers); + expect(textRendered).toContain(repoPropsData.list[0].size); + }); + + it('should be possible to delete a registry', () => { + expect( + vm.$el.querySelector('.table tbody tr .js-delete-registry'), + ).toBeDefined(); + }); + + describe('pagination', () => { + it('should be possible to change the page', () => { + expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/registry/getters_spec.js b/spec/javascripts/registry/getters_spec.js new file mode 100644 index 00000000000..3d989541881 --- /dev/null +++ b/spec/javascripts/registry/getters_spec.js @@ -0,0 +1,43 @@ +import * as getters from '~/registry/stores/getters'; + +describe('Getters Registry Store', () => { + let state; + + beforeEach(() => { + state = { + isLoading: false, + endpoint: '/root/empty-project/container_registry.json', + repos: [{ + canDelete: true, + destroyPath: 'bar', + id: '134', + isLoading: false, + list: [], + location: 'foo', + name: 'gitlab-org/omnibus-gitlab/foo', + tagsPath: 'foo', + }, { + canDelete: true, + destroyPath: 'bar', + id: '123', + isLoading: false, + list: [], + location: 'foo', + name: 'gitlab-org/omnibus-gitlab', + tagsPath: 'foo', + }], + }; + }); + + describe('isLoading', () => { + it('should return the isLoading property', () => { + expect(getters.isLoading(state)).toEqual(state.isLoading); + }); + }); + + describe('repos', () => { + it('should return the repos', () => { + expect(getters.repos(state)).toEqual(state.repos); + }); + }); +}); diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js new file mode 100644 index 00000000000..18600d00bff --- /dev/null +++ b/spec/javascripts/registry/mock_data.js @@ -0,0 +1,122 @@ +export const defaultState = { + isLoading: false, + endpoint: '', + repos: [], +}; + +export const reposServerResponse = [ + { + destroy_path: 'path', + id: '123', + location: 'location', + path: 'foo', + tags_path: 'tags_path', + }, + { + destroy_path: 'path_', + id: '456', + location: 'location_', + path: 'bar', + tags_path: 'tags_path_', + }, +]; + +export const registryServerResponse = [ + { + name: 'centos7', + short_revision: 'b118ab5b0', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + size: 679, + layers: 19, + location: 'location', + created_at: 1505828744434, + destroy_path: 'path_', + }, + { + name: 'centos6', + short_revision: 'b118ab5b0', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + size: 679, + layers: 19, + location: 'location', + created_at: 1505828744434, + }]; + +export const parsedReposServerResponse = [ + { + canDelete: true, + destroyPath: reposServerResponse[0].destroy_path, + id: reposServerResponse[0].id, + isLoading: false, + list: [], + location: reposServerResponse[0].location, + name: reposServerResponse[0].path, + tagsPath: reposServerResponse[0].tags_path, + }, + { + canDelete: true, + destroyPath: reposServerResponse[1].destroy_path, + id: reposServerResponse[1].id, + isLoading: false, + list: [], + location: reposServerResponse[1].location, + name: reposServerResponse[1].path, + tagsPath: reposServerResponse[1].tags_path, + }, +]; + +export const parsedRegistryServerResponse = [ + { + tag: registryServerResponse[0].name, + revision: registryServerResponse[0].revision, + shortRevision: registryServerResponse[0].short_revision, + size: registryServerResponse[0].size, + layers: registryServerResponse[0].layers, + location: registryServerResponse[0].location, + createdAt: registryServerResponse[0].created_at, + destroyPath: registryServerResponse[0].destroy_path, + canDelete: true, + }, + { + tag: registryServerResponse[1].name, + revision: registryServerResponse[1].revision, + shortRevision: registryServerResponse[1].short_revision, + size: registryServerResponse[1].size, + layers: registryServerResponse[1].layers, + location: registryServerResponse[1].location, + createdAt: registryServerResponse[1].created_at, + destroyPath: registryServerResponse[1].destroy_path, + canDelete: false, + }, +]; + +export const repoPropsData = { + canDelete: true, + destroyPath: 'path', + id: '123', + isLoading: false, + list: [ + { + tag: 'centos6', + revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43', + shortRevision: 'b118ab5b0', + size: 19, + layers: 10, + location: 'location', + createdAt: 1505828744434, + destroyPath: 'path', + canDelete: true, + }, + ], + location: 'location', + name: 'foo', + tagsPath: 'path', + pagination: { + perPage: 5, + page: 1, + total: 13, + totalPages: 1, + nextPage: null, + previousPage: null, + }, +}; diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js new file mode 100644 index 00000000000..3c9da4f107b --- /dev/null +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import _ from 'underscore'; +import * as actions from '~/registry/stores/actions'; +import * as types from '~/registry/stores/mutation_types'; +import testAction from '../../helpers/vuex_action_helper'; +import { + defaultState, + reposServerResponse, + registryServerResponse, + parsedReposServerResponse, +} from '../mock_data'; + +Vue.use(VueResource); + +describe('Actions Registry Store', () => { + let interceptor; + let mockedState; + + beforeEach(() => { + mockedState = defaultState; + }); + + describe('server requests', () => { + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + describe('fetchRepos', () => { + beforeEach(() => { + interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + })); + }; + + Vue.http.interceptors.push(interceptor); + }); + + it('should set receveived repos', (done) => { + testAction(actions.fetchRepos, null, mockedState, [ + { type: types.TOGGLE_MAIN_LOADING }, + { type: types.SET_REPOS_LIST, payload: reposServerResponse }, + ], done); + }); + }); + + describe('fetchList', () => { + beforeEach(() => { + interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(registryServerResponse), { + status: 200, + })); + }; + + Vue.http.interceptors.push(interceptor); + }); + + it('should set received list', (done) => { + mockedState.repos = parsedReposServerResponse; + + testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [ + { type: types.TOGGLE_REGISTRY_LIST_LOADING }, + { type: types.SET_REGISTRY_LIST, payload: registryServerResponse }, + ], done); + }); + }); + }); + + describe('setMainEndpoint', () => { + it('should commit set main endpoint', (done) => { + testAction(actions.setMainEndpoint, 'endpoint', mockedState, [ + { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }, + ], done); + }); + }); + + describe('toggleLoading', () => { + it('should commit toggle main loading', (done) => { + testAction(actions.toggleLoading, null, mockedState, [ + { type: types.TOGGLE_MAIN_LOADING }, + ], done); + }); + }); +}); diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/javascripts/registry/stores/mutations_spec.js new file mode 100644 index 00000000000..2e4c0659daa --- /dev/null +++ b/spec/javascripts/registry/stores/mutations_spec.js @@ -0,0 +1,81 @@ +import mutations from '~/registry/stores/mutations'; +import * as types from '~/registry/stores/mutation_types'; +import { + defaultState, + reposServerResponse, + registryServerResponse, + parsedReposServerResponse, + parsedRegistryServerResponse, +} from '../mock_data'; + +describe('Mutations Registry Store', () => { + let mockState; + beforeEach(() => { + mockState = defaultState; + }); + + describe('SET_MAIN_ENDPOINT', () => { + it('should set the main endpoint', () => { + const expectedState = Object.assign({}, mockState, { endpoint: 'foo' }); + mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo'); + expect(mockState).toEqual(expectedState); + }); + }); + + describe('SET_REPOS_LIST', () => { + it('should set a parsed repository list', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + expect(mockState.repos).toEqual(parsedReposServerResponse); + }); + }); + + describe('TOGGLE_MAIN_LOADING', () => { + it('should set a parsed repository list', () => { + mutations[types.TOGGLE_MAIN_LOADING](mockState); + expect(mockState.isLoading).toEqual(true); + }); + }); + + describe('SET_REGISTRY_LIST', () => { + it('should set a list of registries in a specific repository', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); + + expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse); + expect(mockState.repos[0].pagination).toEqual({ + perPage: 2, + page: 1, + total: 10, + totalPages: NaN, + nextPage: NaN, + previousPage: NaN, + }); + }); + }); + + describe('TOGGLE_REGISTRY_LIST_LOADING', () => { + it('should toggle isLoading property for a specific repository', () => { + mutations[types.SET_REPOS_LIST](mockState, reposServerResponse); + mutations[types.SET_REGISTRY_LIST](mockState, { + repo: mockState.repos[0], + resp: registryServerResponse, + headers: { + 'x-per-page': 2, + 'x-page': 1, + 'x-total': 10, + }, + }); + + mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]); + expect(mockState.repos[0].isLoading).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 2422e844e97..c83b947579b 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -95,38 +95,87 @@ describe('MRWidgetReadyToMerge', () => { }); }); + describe('status', () => { + it('defaults to success', () => { + vm.mr.pipeline = true; + expect(vm.status).toEqual('success'); + }); + + it('returns failed when MR has CI but also has an unknown status', () => { + vm.mr.hasCI = true; + expect(vm.status).toEqual('failed'); + }); + + it('returns default when MR has no pipeline', () => { + expect(vm.status).toEqual('success'); + }); + + it('returns pending when pipeline is active', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.status).toEqual('pending'); + }); + + it('returns failed when pipeline is failed', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineFailed = true; + expect(vm.status).toEqual('failed'); + }); + }); + describe('mergeButtonClass', () => { const defaultClass = 'btn btn-sm btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; - it('should return default class', () => { + it('defaults to success class', () => { + expect(vm.mergeButtonClass).toEqual(defaultClass); + }); + + it('returns success class for success status', () => { vm.mr.pipeline = true; expect(vm.mergeButtonClass).toEqual(defaultClass); }); - it('should return failed class when MR has CI but also has an unknown status', () => { - vm.mr.hasCI = true; - expect(vm.mergeButtonClass).toEqual(failedClass); - }); - - it('should return default class when MR has no pipeline', () => { - expect(vm.mergeButtonClass).toEqual(defaultClass); - }); - - it('should return in action class when pipeline is active', () => { + it('returns info class for pending status', () => { vm.mr.pipeline = {}; vm.mr.isPipelineActive = true; expect(vm.mergeButtonClass).toEqual(inActionClass); }); - it('should return failed class when pipeline is failed', () => { - vm.mr.pipeline = {}; - vm.mr.isPipelineFailed = true; + it('returns failed class for failed status', () => { + vm.mr.hasCI = true; expect(vm.mergeButtonClass).toEqual(failedClass); }); }); + describe('status icon', () => { + it('defaults to tick icon', () => { + expect(vm.iconClass).toEqual('success'); + }); + + it('shows tick for success status', () => { + vm.mr.pipeline = true; + expect(vm.iconClass).toEqual('success'); + }); + + it('shows tick for pending status', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.iconClass).toEqual('success'); + }); + + it('shows x for failed status', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); + }); + + it('shows x for merge not allowed', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); + }); + }); + describe('mergeButtonText', () => { it('should return Merge', () => { expect(vm.mergeButtonText).toEqual('Merge'); @@ -177,7 +226,7 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.isMergeButtonDisabled).toBeTruthy(); }); - it('should return true when there vm instance is making request', () => { + it('should return true when the vm instance is making request', () => { vm.isMakingRequest = true; expect(vm.isMergeButtonDisabled).toBeTruthy(); }); diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a0482e30a33..5f12125beb2 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1444,6 +1444,51 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#rm_branch' do + shared_examples "user deleting a branch" do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + let(:user) { create(:user) } + let(:branch_name) { "to-be-deleted-soon" } + + before do + project.team << [user, :developer] + repository.create_branch(branch_name) + end + + it "removes the branch from the repo" do + repository.rm_branch(branch_name, user: user) + + expect(repository.rugged.branches[branch_name]).to be_nil + end + end + + context "when Gitaly user_delete_branch is enabled" do + it_behaves_like "user deleting a branch" + end + + context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do + it_behaves_like "user deleting a branch" + end + end + + describe '#write_ref' do + context 'validations' do + using RSpec::Parameterized::TableSyntax + + where(:ref_path, :ref) do + 'foo bar' | '123' + 'foobar' | "12\x003" + end + + with_them do + it 'raises ArgumentError' do + expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError) + end + end + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 769b14687ac..7bd6a7fa842 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -4,10 +4,10 @@ describe Gitlab::GitalyClient::OperationService do let(:project) { create(:project) } let(:repository) { project.repository.raw } let(:client) { described_class.new(repository) } + let(:user) { create(:user) } + let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } describe '#user_create_branch' do - let(:user) { create(:user) } - let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } let(:branch_name) { 'new' } let(:start_point) { 'master' } let(:request) do @@ -52,4 +52,41 @@ describe Gitlab::GitalyClient::OperationService do end end end + + describe '#user_delete_branch' do + let(:branch_name) { 'my-branch' } + let(:request) do + Gitaly::UserDeleteBranchRequest.new( + repository: repository.gitaly_repository, + branch_name: branch_name, + user: gitaly_user + ) + end + let(:response) { Gitaly::UserDeleteBranchResponse.new } + + subject { client.user_delete_branch(branch_name, user) } + + it 'sends a user_delete_branch message' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + subject + end + + context "when pre_receive_error is present" do + let(:response) do + Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::HooksService::PreReceiveError, "something failed") + end + end + end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index baf8f6644bf..8026fba9f0a 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -22,5 +22,12 @@ describe Gitlab::SQL::Union do expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end + + it 'uses UNION ALL when removing duplicates is disabled' do + union = described_class + .new([relation_1, relation_2], remove_duplicates: false) + + expect(union.to_sql).to include('UNION ALL') + end end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 8eabc4ca72f..81c2057e175 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -155,5 +155,15 @@ describe Key, :mailer do it 'strips white spaces' do expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end + + it 'invalidates the public_key attribute' do + key = build(:key) + + original = key.public_key + key.key = valid_key + + expect(original.key_text).not_to be_nil + expect(key.public_key.key_text).to eq(valid_key) + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d80d5657c42..188a0a98ec3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -791,6 +791,49 @@ describe MergeRequest do end end + describe '#has_ci?' do + let(:merge_request) { build_stubbed(:merge_request) } + + context 'has ci' do + it 'returns true if MR has head_pipeline_id and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { double } + allow(merge_request).to receive(:has_no_commits?) { false } + + expect(merge_request.has_ci?).to be(true) + end + + it 'returns true if MR has any pipeline and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:has_no_commits?) { false } + allow(merge_request).to receive(:all_pipelines) { [double] } + + expect(merge_request.has_ci?).to be(true) + end + + it 'returns true if MR has CI service and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:has_no_commits?) { false } + allow(merge_request).to receive(:all_pipelines) { [] } + + expect(merge_request.has_ci?).to be(true) + end + end + + context 'has no ci' do + it 'returns false if MR has no CI service nor pipeline, and no commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:all_pipelines) { [] } + allow(merge_request).to receive(:has_no_commits?) { true } + + expect(merge_request.has_ci?).to be(false) + end + end + end + describe '#all_pipelines' do shared_examples 'returning pipelines with proper ordering' do let!(:all_pipelines) do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 8a4dcdc311e..7156c1b7aa8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -636,18 +636,18 @@ describe Repository do describe '#fetch_ref' do describe 'when storage is broken', broken_storage: true do it 'should raise a storage error' do - path = broken_repository.path_to_repo - - expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') } + expect_to_raise_storage_error do + broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2') + end end end end describe '#create_ref' do - it 'redirects the call to fetch_ref' do + it 'redirects the call to write_ref' do ref, ref_path = '1', '2' - expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path) + expect(repository.raw_repository).to receive(:write_ref).with(ref_path, ref) repository.create_ref(ref, ref_path) end @@ -901,47 +901,6 @@ describe Repository do end end - describe '#rm_branch' do - let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature - let(:blank_sha) { '0000000000000000000000000000000000000000' } - - context 'when pre hooks were successful' do - it 'runs without errors' do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') - - expect { repository.rm_branch(user, 'feature') }.not_to raise_error - end - - it 'deletes the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - - expect { repository.rm_branch(user, 'feature') }.not_to raise_error - - expect(repository.find_branch('feature')).to be_nil - end - end - - context 'when pre hooks failed' do - it 'gets an error' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - end - - it 'does not delete the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - expect(repository.find_branch('feature')).not_to be_nil - end - end - end - describe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev @@ -1744,13 +1703,75 @@ describe Repository do end describe '#rm_branch' do - let(:user) { create(:user) } + shared_examples "user deleting a branch" do + it 'removes a branch' do + expect(repository).to receive(:before_remove_branch) + expect(repository).to receive(:after_remove_branch) - it 'removes a branch' do - expect(repository).to receive(:before_remove_branch) - expect(repository).to receive(:after_remove_branch) + repository.rm_branch(user, 'feature') + end + end - repository.rm_branch(user, 'feature') + context 'with gitaly enabled' do + it_behaves_like "user deleting a branch" + + context 'when pre hooks failed' do + before do + allow_any_instance_of(Gitlab::GitalyClient::OperationService) + .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError) + end + + it 'gets an error and does not delete the branch' do + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + + expect(repository.find_branch('feature')).not_to be_nil + end + end + end + + context 'with gitaly disabled', skip_gitaly_mock: true do + it_behaves_like "user deleting a branch" + + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:blank_sha) { '0000000000000000000000000000000000000000' } + + context 'when pre hooks were successful' do + it 'runs without errors' do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) + .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') + + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + end + + it 'deletes the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) + + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + + expect(repository.find_branch('feature')).to be_nil + end + end + + context 'when pre hooks failed' do + it 'gets an error' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end + + it 'does not delete the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + expect(repository.find_branch('feature')).not_to be_nil + end + end end end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 2187be0190d..5e114434a67 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -300,6 +300,10 @@ describe MergeRequestPresenter do described_class.new(resource, current_user: user).remove_wip_path end + before do + allow(resource).to receive(:work_in_progress?).and_return(true) + end + context 'when merge request enabled and has permission' do it 'has remove_wip_path' do allow(project).to receive(:merge_requests_enabled?) { true } diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb new file mode 100644 index 00000000000..c589cd18f77 --- /dev/null +++ b/spec/serializers/container_repository_entity_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe ContainerRepositoryEntity do + let(:entity) do + described_class.new(repository, request: request) + end + + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:repository) { create(:container_repository, project: project) } + + let(:request) { double('request') } + + subject { entity.as_json } + + before do + stub_container_registry_config(enabled: true) + allow(request).to receive(:project).and_return(project) + allow(request).to receive(:current_user).and_return(user) + end + + it 'exposes required informations' do + expect(subject).to include(:id, :path, :location, :tags_path) + end + + context 'when user can manage repositories' do + before do + project.add_developer(user) + end + + it 'exposes destroy_path' do + expect(subject).to include(:destroy_path) + end + end + + context 'when user cannot manage repositories' do + it 'does not expose destroy_path' do + expect(subject).not_to include(:destroy_path) + end + end +end diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb new file mode 100644 index 00000000000..6dcc5204516 --- /dev/null +++ b/spec/serializers/container_tag_entity_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe ContainerTagEntity do + let(:entity) do + described_class.new(tag, request: request) + end + + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:repository) { create(:container_repository, name: 'image', project: project) } + + let(:request) { double('request') } + let(:tag) { repository.tag('test') } + + subject { entity.as_json } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: /image/, tags: %w[test]) + allow(request).to receive(:project).and_return(project) + allow(request).to receive(:current_user).and_return(user) + end + + it 'exposes required informations' do + expect(subject).to include(:name, :location, :revision, :total_size, :created_at) + end + + context 'when user can manage repositories' do + before do + project.add_developer(user) + end + + it 'exposes destroy_path' do + expect(subject).to include(:destroy_path) + end + end + + context 'when user cannot manage repositories' do + it 'does not expose destroy_path' do + expect(subject).not_to include(:destroy_path) + end + end +end diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb index 4288955ddbc..4aeb593da44 100644 --- a/spec/serializers/merge_request_entity_spec.rb +++ b/spec/serializers/merge_request_entity_spec.rb @@ -11,16 +11,6 @@ describe MergeRequestEntity do described_class.new(resource, request: request).as_json end - it 'includes author' do - req = double('request') - - author_payload = UserEntity - .represent(resource.author, request: req) - .as_json - - expect(subject[:author]).to eq(author_payload) - end - it 'includes pipeline' do req = double('request', current_user: user) pipeline = build_stubbed(:ci_pipeline) diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 78a2ff73746..5f22d886910 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -39,11 +39,11 @@ module StubGitlabCalls .and_return({ 'tags' => tags }) allow_any_instance_of(ContainerRegistry::Client) - .to receive(:repository_manifest).with(repository) + .to receive(:repository_manifest).with(repository, anything) .and_return(stub_container_registry_tag_manifest) allow_any_instance_of(ContainerRegistry::Client) - .to receive(:blob).with(repository) + .to receive(:blob).with(repository, anything, 'application/octet-stream') .and_return(stub_container_registry_blob) end diff --git a/spec/views/projects/registry/repositories/index.html.haml_spec.rb b/spec/views/projects/registry/repositories/index.html.haml_spec.rb deleted file mode 100644 index cf0aa44a4a2..00000000000 --- a/spec/views/projects/registry/repositories/index.html.haml_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'spec_helper' - -describe 'projects/registry/repositories/index' do - let(:group) { create(:group, path: 'group') } - let(:project) { create(:project, group: group, path: 'test') } - - let(:repository) do - create(:container_repository, project: project, name: 'image') - end - - before do - stub_container_registry_config(enabled: true, - host_port: 'registry.gitlab', - api_url: 'http://registry.gitlab') - - stub_container_registry_tags(repository: :any, tags: [:latest]) - - assign(:project, project) - assign(:images, [repository]) - - allow(view).to receive(:can?).and_return(true) - end - - it 'contains container repository path' do - render - - expect(rendered).to have_content 'group/test/image' - end - - it 'contains attribute for copying tag location into clipboard' do - render - - expect(rendered).to have_css 'button[data-clipboard-text="docker pull ' \ - 'registry.gitlab/group/test/image:latest"]' - end -end