summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes1
-rw-r--r--.gitlab-ci.yml18
-rw-r--r--CONTRIBUTING.md53
-rw-r--r--Dangerfile6
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js5
-rw-r--r--app/assets/javascripts/diffs/components/app.vue16
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue41
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue41
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue26
-rw-r--r--app/assets/javascripts/diffs/store/actions.js27
-rw-r--r--app/assets/javascripts/diffs/store/getters.js50
-rw-r--r--app/assets/javascripts/ide/components/ide.vue3
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue21
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue37
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/button.vue51
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue70
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue72
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue124
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue11
-rw-r--r--app/assets/javascripts/ide/stores/actions.js13
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js5
-rw-r--r--app/assets/javascripts/ide/stores/state.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js7
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js5
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js12
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue98
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue7
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss7
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/repo.scss48
-rw-r--r--app/assets/stylesheets/pages/todos.scss16
-rw-r--r--app/controllers/application_controller.rb12
-rw-r--r--app/controllers/concerns/todos_actions.rb12
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/import/manifest_controller.rb93
-rw-r--r--app/controllers/projects/todos_controller.rb14
-rw-r--r--app/finders/todos_finder.rb52
-rw-r--r--app/helpers/issuables_helper.rb13
-rw-r--r--app/helpers/namespaces_helper.rb11
-rw-r--r--app/helpers/todos_helper.rb10
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/group.rb8
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/notification_setting.rb1
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_wiki.rb5
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/todo.rb11
-rw-r--r--app/services/groups/nested_create_service.rb12
-rw-r--r--app/services/metrics_service.rb21
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/views/admin/groups/_form.html.haml10
-rw-r--r--app/views/admin/groups/_group.html.haml4
-rw-r--r--app/views/admin/groups/edit.html.haml4
-rw-r--r--app/views/admin/groups/index.html.haml4
-rw-r--r--app/views/admin/groups/new.html.haml4
-rw-r--r--app/views/admin/groups/show.html.haml56
-rw-r--r--app/views/dashboard/_activities.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml48
-rw-r--r--app/views/explore/_head.html.haml4
-rw-r--r--app/views/explore/groups/_nav.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml12
-rw-r--r--app/views/explore/projects/_filter.html.haml6
-rw-r--r--app/views/explore/projects/_nav.html.haml6
-rw-r--r--app/views/explore/projects/index.html.haml4
-rw-r--r--app/views/explore/projects/starred.html.haml4
-rw-r--r--app/views/explore/projects/trending.html.haml4
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/import/_githubish_status.html.haml20
-rw-r--r--app/views/import/_project_status.html.haml11
-rw-r--r--app/views/import/manifest/_form.html.haml23
-rw-r--r--app/views/import/manifest/new.html.haml12
-rw-r--r--app/views/import/manifest/status.html.haml42
-rw-r--r--app/views/projects/_activity.html.haml3
-rw-r--r--app/views/projects/_import_project_pane.html.haml47
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml2
-rw-r--r--app/views/projects/new.html.haml13
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rwxr-xr-xbin/changelog14
-rw-r--r--changelogs/unreleased/46930-fix-updated_at-if-created_at-is-set-note-api.yml5
-rw-r--r--changelogs/unreleased/48237-toggle-file-comments.yml5
-rw-r--r--changelogs/unreleased/48789-remove-event-listeners-scroll.yml6
-rw-r--r--changelogs/unreleased/48894-fix-rss-button-interaction.yml5
-rw-r--r--changelogs/unreleased/48934.yml5
-rw-r--r--changelogs/unreleased/an-no-healthcheck-until-brooklyn.yml5
-rw-r--r--changelogs/unreleased/dz-manifest-import.yml5
-rw-r--r--changelogs/unreleased/fix-gb-add-missing-before-sha-predefined-variable.yml5
-rw-r--r--changelogs/unreleased/fix-gb-fix-project-settings-build-time-validation.yml5
-rw-r--r--changelogs/unreleased/fix-performance-problem-of-tags-query.yml5
-rw-r--r--changelogs/unreleased/gitaly-serverservice-info-timeout.yml5
-rw-r--r--changelogs/unreleased/ide-pipeline-icon-open.yml5
-rw-r--r--changelogs/unreleased/ide-row-dropdown-design-update.yml5
-rw-r--r--changelogs/unreleased/issue_47709.yml5
-rw-r--r--changelogs/unreleased/sh-fix-stderr-pipe-consumption.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-wiki-empty-check.yml5
-rw-r--r--changelogs/unreleased/winh-upgrade-grape-path-helpers.yml5
-rw-r--r--config/routes/import.rb6
-rw-r--r--danger/changelog/Dangerfile66
-rw-r--r--danger/changes_size/Dangerfile17
-rw-r--r--danger/database/Dangerfile83
-rw-r--r--danger/gemfile/Dangerfile24
-rw-r--r--danger/metadata/Dangerfile25
-rw-r--r--danger/specs/Dangerfile12
-rw-r--r--db/fixtures/development/19_environments.rb2
-rw-r--r--db/migrate/20180608091413_add_group_to_todos.rb32
-rw-r--r--db/schema.rb5
-rw-r--r--doc/api/README.md32
-rw-r--r--doc/api/notes.md2
-rw-r--r--doc/api/project_import_export.md28
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/todos.md1
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/development/fe_guide/img/vue_arch.pngbin9848 -> 0 bytes
-rw-r--r--doc/development/fe_guide/vue.md239
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md2
-rw-r--r--doc/user/project/import/img/manifest_status.pngbin0 -> 34878 bytes
-rw-r--r--doc/user/project/import/img/manifest_upload.pngbin0 -> 12079 bytes
-rw-r--r--doc/user/project/import/index.md1
-rw-r--r--doc/user/project/import/manifest.md48
-rw-r--r--doc/user/project/issue_board.md10
-rw-r--r--doc/workflow/todos.md1
-rw-r--r--lib/api/entities.rb28
-rw-r--r--lib/api/helpers/notes_helpers.rb2
-rw-r--r--lib/api/settings.rb2
-rw-r--r--lib/gitaly/server.rb2
-rw-r--r--lib/gitlab/git/popen.rb18
-rw-r--r--lib/gitlab/git/repository.rb184
-rw-r--r--lib/gitlab/git/rev_list.rb33
-rw-r--r--lib/gitlab/gitaly_client/server_service.rb2
-rw-r--r--lib/gitlab/import_sources.rb3
-rw-r--r--lib/gitlab/kubernetes.rb7
-rw-r--r--lib/gitlab/manifest_import/manifest.rb81
-rw-r--r--lib/gitlab/manifest_import/project_creator.rb41
-rw-r--r--lib/gitlab/popen.rb9
-rw-r--r--locale/gitlab.pot129
-rw-r--r--qa/README.md2
-rw-r--r--qa/qa.rb14
-rw-r--r--qa/qa/factory/repository/project_push.rb2
-rw-r--r--qa/qa/factory/repository/push.rb16
-rw-r--r--qa/qa/factory/resource/fork.rb24
-rw-r--r--qa/qa/factory/resource/kubernetes_cluster.rb3
-rw-r--r--qa/qa/factory/resource/merge_request_from_fork.rb24
-rw-r--r--qa/qa/factory/resource/project.rb1
-rw-r--r--qa/qa/factory/resource/user.rb34
-rw-r--r--qa/qa/page/issuable/sidebar.rb7
-rw-r--r--qa/qa/page/layout/banner.rb17
-rw-r--r--qa/qa/page/main/login.rb19
-rw-r--r--qa/qa/page/main/sign_up.rb27
-rw-r--r--qa/qa/page/menu/main.rb4
-rw-r--r--qa/qa/page/merge_request/show.rb6
-rw-r--r--qa/qa/page/project/fork/new.rb17
-rw-r--r--qa/qa/page/project/new.rb5
-rw-r--r--qa/qa/page/project/show.rb9
-rw-r--r--qa/qa/runtime/browser.rb4
-rw-r--r--qa/qa/runtime/namespace.rb2
-rw-r--r--qa/qa/specs/features/login/standard_spec.rb15
-rw-r--r--qa/qa/specs/features/merge_request/create_spec.rb11
-rw-r--r--qa/qa/specs/features/project/fork_project_spec.rb23
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb24
-rw-r--r--spec/bin/changelog_spec.rb14
-rw-r--r--spec/controllers/metrics_controller_spec.rb49
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb133
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/features/groups_spec.rb6
-rw-r--r--spec/features/import/manifest_import_spec.rb51
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb6
-rw-r--r--spec/features/projects/new_project_spec.rb27
-rw-r--r--spec/features/projects/settings/user_transfers_a_project_spec.rb9
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb32
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb8
-rw-r--r--spec/features/projects/tree/create_file_spec.rb4
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb4
-rw-r--r--spec/features/projects_spec.rb6
-rw-r--r--spec/finders/todos_finder_spec.rb64
-rw-r--r--spec/fixtures/aosp_manifest.xml685
-rw-r--r--spec/helpers/issuables_helper_spec.rb21
-rw-r--r--spec/helpers/namespaces_helper_spec.rb10
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js144
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js44
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js123
-rw-r--r--spec/javascripts/frequent_items/store/actions_spec.js4
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js114
-rw-r--r--spec/javascripts/helpers/vuex_action_helper_spec.js141
-rw-r--r--spec/javascripts/ide/components/ide_status_bar_spec.js26
-rw-r--r--spec/javascripts/ide/components/new_dropdown/button_spec.js49
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js46
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js36
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js3
-rw-r--r--spec/javascripts/ide/mock_data.js3
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js5
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js15
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js7
-rw-r--r--spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js25
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/actions_spec.js6
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js201
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js13
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js14
-rw-r--r--spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js243
-rw-r--r--spec/javascripts/sidebar/todo_spec.js158
-rw-r--r--spec/javascripts/test_bundle.js4
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb8
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb9
-rw-r--r--spec/lib/gitlab/git/popen_spec.rb30
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb168
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb61
-rw-r--r--spec/lib/gitlab/git_access_spec.rb12
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb10
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb15
-rw-r--r--spec/lib/gitlab/manifest_import/manifest_spec.rb46
-rw-r--r--spec/lib/gitlab/manifest_import/project_creator_spec.rb33
-rw-r--r--spec/lib/gitlab/popen_spec.rb13
-rw-r--r--spec/models/ci/build_spec.rb1
-rw-r--r--spec/models/notification_setting_spec.rb6
-rw-r--r--spec/models/project_spec.rb14
-rw-r--r--spec/models/project_wiki_spec.rb10
-rw-r--r--spec/models/repository_spec.rb29
-rw-r--r--spec/models/todo_spec.rb1
-rw-r--r--spec/support/shared_examples/controllers/todos_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/requests/api/notes.rb1
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb7
233 files changed, 4013 insertions, 2269 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000000..f1c41c9bb76
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+Dangerfile gitlab-language=ruby
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 610a5ecba6d..137c26d7dae 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -348,6 +348,24 @@ retrieve-tests-metadata:
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
+danger-review:
+ image: registry.gitlab.com/gitlab-org/gitaly/dangercontainer:latest
+ stage: prepare
+ before_script:
+ - source scripts/utils.sh
+ - retry gem install danger --no-ri --no-rdoc
+ cache: {}
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ except:
+ variables:
+ - $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/
+ script:
+ - git version
+ - danger --fail-on-errors=true
+
update-tests-metadata:
<<: *tests-metadata-state
<<: *only-canonical-masters
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fd4e769ecee..4a1fa39b41d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,6 +50,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Definition of done](#definition-of-done)
- [Style guides](#style-guides)
- [Code of conduct](#code-of-conduct)
+- [Contribution Flow](#contribution-flow)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -225,24 +226,24 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
-### Bug Priority labels
+### Priority labels
-Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
+Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
-| Label | Meaning | Estimate time to fix | Guidance |
-|-------|-----------------|------------------------------------------------------------------|----------|
-| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
-| ~P2 | High Priority | The next release | |
-| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
-| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
+| Label | Meaning | Estimate time to fix |
+|-------|-----------------|------------------------------------------------------------------|
+| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com |
+| ~P2 | High Priority | The next release |
+| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) |
+| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) |
-### Bug Severity labels
+### Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users.
-| Label | Meaning | Impact of the defect | Example |
+| Label | Meaning | Impact on Functionality | Example |
|-------|-------------------|-------------------------------------------------------|---------|
| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
@@ -251,12 +252,14 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
#### Severity impact guidance
-| Label | Security Impact | Availability / Performance Impact |
-|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
-| ~S1 | >50% users impacted (possible company extinction level event) | |
-| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
-| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
-| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
+Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline.
+
+| Severity | Affected Customers/Users | GitLab.com Availability | Performance Degradation |
+|----------|---------------------------------------------------------------------|----------------------------------------------------|------------------------------|
+| ~S1 | >50% users affected (possible company extinction level event) | Significant impact on all of GitLab.com | |
+| ~S2 | Many users or multiple paid customers affected (but not apocalyptic)| Significant impact on large portions of GitLab.com | Degradation is guaranteed to occur in the near future |
+| ~S3 | A few users or a single paid customer affected | Limited impact on important portions of GitLab.com | Degradation is likely to occur in the near future |
+| ~S4 | No paid users/customer affected, or expected to in the near future | Minor impact on on GitLab.com | Degradation _may_ occur but it's not likely |
### Label for community contributors
@@ -729,6 +732,24 @@ reported by emailing `contact@gitlab.com`.
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
+## Contribution Flow
+
+When contributing to GitLab, your merge request is subject to review by merge request maintainers of a particular specialty.
+
+When you submit code to GitLab, we really want it to get merged, but there will be times when it will not be merged.
+
+When maintainers are reading through a merge request they may request guidance from other maintainers. If merge request maintainers conclude that the code should not be merged, our reasons will be fully disclosed. If it has been decided that the code quality is not up to GitLab’s standards, the merge request maintainer will refer the author to our docs and code style guides, and provide some guidance.
+
+Sometimes style guides will be followed but the code will lack structural integrity, or the maintainer will have reservations about the code’s overall quality. When there is a reservation the maintainer will inform the author and provide some guidance. The author may then choose to update the merge request. Once the merge request has been updated and reassigned to the maintainer, they will review the code again. Once the code has been resubmitted any number of times, the maintainer may choose to close the merge request with a summary of why it will not be merged, as well as some guidance. If the merge request is closed the maintainer will be open to discussion as to how to improve the code so it can be approved in the future.
+
+GitLab will do its best to review community contributions as quickly as possible. Specially appointed developers review community contributions daily. You may take a look at the [team page](https://about.gitlab.com/team/) for the merge request coach who specializes in the type of code you have written and mention them in the merge request. For example, if you have written some JavaScript in your code then you should mention the frontend merge request coach. If your code has multiple disciplines you may mention multiple merge request coaches.
+
+GitLab receives a lot of community contributions, so if your code has not been reviewed within 4 days of its initial submission feel free to re-mention the appropriate merge request coach.
+
+When submitting code to GitLab, you may feel that your contribution requires the aid of an external library. If your code includes an external library please provide a link to the library, as well as reasons for including it.
+
+When your code contains more than 500 changes, any major breaking changes, or an external library, `@mention` a maintainer in the merge request. If you are not sure who to mention, the reviewer will add one early in the merge request process.
+
[core team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/
diff --git a/Dangerfile b/Dangerfile
new file mode 100644
index 00000000000..84b72673c50
--- /dev/null
+++ b/Dangerfile
@@ -0,0 +1,6 @@
+danger.import_dangerfile(path: 'danger/metadata')
+danger.import_dangerfile(path: 'danger/changes_size')
+danger.import_dangerfile(path: 'danger/changelog')
+danger.import_dangerfile(path: 'danger/specs')
+danger.import_dangerfile(path: 'danger/gemfile')
+danger.import_dangerfile(path: 'danger/database')
diff --git a/Gemfile.lock b/Gemfile.lock
index 1b8a777ceb4..7f9207d9dfe 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -359,7 +359,7 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
- grape-path-helpers (1.0.5)
+ grape-path-helpers (1.0.6)
activesupport (>= 4, < 5.1)
grape (~> 1.0)
rake (~> 12)
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 1638e09132b..b0c85c2572e 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -2,13 +2,16 @@ import $ from 'jquery';
import { rstrip } from './lib/utils/common_utils';
function openConfirmDangerModal($form, text) {
+ const $input = $('.js-confirm-danger-input');
+ $input.val('');
+
$('.js-confirm-text').text(text || '');
- $('.js-confirm-danger-input').val('');
$('#modal-confirm-danger').modal('show');
const confirmTextMatch = $('.js-confirm-danger-match').text();
const $submit = $('.js-confirm-danger-submit');
$submit.disable();
+ $input.focus();
$('.js-confirm-danger-input').off('input').on('input', function handleInput() {
const confirmText = rstrip($(this).val());
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 0327fceb38d..1d1415fe6ca 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -41,11 +41,6 @@ export default {
required: true,
},
},
- data() {
- return {
- activeFile: '',
- };
- },
computed: {
...mapState({
isLoading: state => state.diffs.isLoading,
@@ -126,14 +121,6 @@ export default {
eventHub.$emit('fetchNotesData');
}
},
- setActive(filePath) {
- this.activeFile = filePath;
- },
- unsetActive(filePath) {
- if (this.activeFile === filePath) {
- this.activeFile = '';
- }
- },
adjustView() {
if (this.shouldShow && this.isParallelView) {
window.mrTabs.expandViewContainer();
@@ -195,7 +182,6 @@ export default {
<changed-files
:diff-files="diffFiles"
- :active-file="activeFile"
/>
<div
@@ -207,8 +193,6 @@ export default {
:key="file.newPath"
:file="file"
:current-user="currentUser"
- @setActive="setActive(file.filePath)"
- @unsetActive="unsetActive(file.filePath)"
/>
</div>
<no-changes v-else />
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
index 9d29357d800..97751db1254 100644
--- a/app/assets/javascripts/diffs/components/changed_files.vue
+++ b/app/assets/javascripts/diffs/components/changed_files.vue
@@ -16,13 +16,6 @@ export default {
ClipboardButton,
},
mixins: [changedFilesMixin],
- props: {
- activeFile: {
- type: String,
- required: false,
- default: '',
- },
- },
data() {
return {
isStuck: false,
@@ -70,7 +63,7 @@ export default {
pluralize,
handleScroll() {
if (!this.updating) {
- requestAnimationFrame(this.updateIsStuck);
+ this.$nextTick(this.updateIsStuck);
this.updating = true;
}
},
@@ -148,25 +141,8 @@ export default {
/>
<span
- v-show="activeFile"
- class="prepend-left-5"
- >
- <strong class="prepend-right-5">
- {{ truncatedDiffPath(activeFile) }}
- </strong>
- <clipboard-button
- :text="activeFile"
- :title="s__('Copy file name to clipboard')"
- tooltip-placement="bottom"
- tooltip-container="body"
- class="btn btn-default btn-transparent btn-clipboard"
- />
- </span>
-
- <span
- v-show="!isStuck"
- id="diff-stats"
- class="diff-stats-additions-deletions-expanded"
+ class="js-diff-stats-additions-deletions-expanded
+ diff-stats-additions-deletions-expanded"
>
with
<strong class="cgreen">
@@ -177,6 +153,17 @@ export default {
{{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
</strong>
</span>
+ <div
+ class="js-diff-stats-additions-deletions-collapsed
+ diff-stats-additions-deletions-collapsed float-right d-sm-none"
+ >
+ <strong class="cgreen">
+ +{{ sumAddedLines }}
+ </strong>
+ <strong class="cred">
+ -{{ sumRemovedLines }}
+ </strong>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
index b38d217fbe3..045688a32bf 100644
--- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
@@ -40,7 +40,7 @@ export default {
{{ n__('%d changed file', '%d changed files', diffFiles.length) }}
</span>
<icon
- :size="8"
+ class="caret-icon"
name="chevron-down"
/>
</button>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 060386c3ecb..944084f05c9 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -25,15 +25,11 @@ export default {
},
data() {
return {
- isActive: false,
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
};
},
computed: {
- isDiscussionsExpanded() {
- return true; // TODO: @fatihacet - Fix this.
- },
isCollapsed() {
return this.file.collapsed || false;
},
@@ -51,12 +47,6 @@ export default {
return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
},
},
- mounted() {
- document.addEventListener('scroll', this.handleScroll);
- },
- beforeDestroy() {
- document.removeEventListener('scroll', this.handleScroll);
- },
methods: {
...mapActions('diffs', ['loadCollapsedDiff']),
handleToggle() {
@@ -68,36 +58,6 @@ export default {
this.file.collapsed = !this.file.collapsed;
}
},
- handleScroll() {
- if (!this.updating) {
- requestAnimationFrame(this.scrollUpdate.bind(this));
- this.updating = true;
- }
- },
- scrollUpdate() {
- const header = document.querySelector('.js-diff-files-changed');
- if (!header) {
- this.updating = false;
- return;
- }
-
- const { top, bottom } = this.$el.getBoundingClientRect();
- const { top: topOfFixedHeader, bottom: bottomOfFixedHeader } = header.getBoundingClientRect();
-
- const headerOverlapsContent = top < topOfFixedHeader && bottom > bottomOfFixedHeader;
- const fullyAboveHeader = bottom < bottomOfFixedHeader;
- const fullyBelowHeader = top > topOfFixedHeader;
-
- if (headerOverlapsContent && !this.isActive) {
- this.$emit('setActive');
- this.isActive = true;
- } else if (this.isActive && (fullyAboveHeader || fullyBelowHeader)) {
- this.$emit('unsetActive');
- this.isActive = false;
- }
-
- this.updating = false;
- },
handleLoadCollapsedDiff() {
this.isLoadingCollapsedDiff = true;
@@ -131,7 +91,6 @@ export default {
:diff-file="file"
:collapsible="true"
:expanded="!isCollapsed"
- :discussions-expanded="isDiscussionsExpanded"
:add-merge-request-buttons="true"
class="js-file-title file-title"
@toggleFile="handleToggle"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 1957698c6c1..c5abd0a9568 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,5 +1,6 @@
<script>
import _ from 'underscore';
+import { mapActions, mapGetters } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -38,11 +39,6 @@ export default {
required: false,
default: true,
},
- discussionsExpanded: {
- type: Boolean,
- required: false,
- default: true,
- },
currentUser: {
type: Object,
required: true,
@@ -54,6 +50,10 @@ export default {
};
},
computed: {
+ ...mapGetters('diffs', ['diffHasExpandedDiscussions']),
+ hasExpandedDiscussions() {
+ return this.diffHasExpandedDiscussions(this.diffFile);
+ },
icon() {
if (this.diffFile.submodule) {
return 'archive';
@@ -88,9 +88,6 @@ export default {
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
- isDiscussionsExpanded() {
- return this.discussionsExpanded && this.expanded;
- },
viewFileButtonText() {
const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
return sprintf(
@@ -113,7 +110,8 @@ export default {
},
},
methods: {
- handleToggle(e, checkTarget) {
+ ...mapActions('diffs', ['toggleFileDiscussions']),
+ handleToggleFile(e, checkTarget) {
if (
!checkTarget ||
e.target === this.$refs.header ||
@@ -125,6 +123,9 @@ export default {
showForkMessage() {
this.$emit('showForkMessage');
},
+ handleToggleDiscussions() {
+ this.toggleFileDiscussions(this.diffFile);
+ },
},
};
</script>
@@ -133,7 +134,7 @@ export default {
<div
ref="header"
class="js-file-title file-title file-title-flex-parent"
- @click="handleToggle($event, true)"
+ @click="handleToggleFile($event, true)"
>
<div class="file-header-content">
<icon
@@ -216,10 +217,11 @@ export default {
v-if="diffFile.blob && diffFile.blob.readableText"
>
<button
- :class="{ active: isDiscussionsExpanded }"
+ :class="{ active: hasExpandedDiscussions }"
:title="s__('MergeRequests|Toggle comments for this file')"
- class="btn js-toggle-diff-comments"
+ class="js-btn-vue-toggle-comments btn"
type="button"
+ @click="handleToggleDiscussions"
>
<icon name="comment" />
</button>
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 5bfe42618c2..27001142257 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -82,5 +82,32 @@ export const expandAllFiles = ({ commit }) => {
commit(types.EXPAND_ALL_FILES);
};
+/**
+ * Toggles the file discussions after user clicked on the toggle discussions button.
+ *
+ * Gets the discussions for the provided diff.
+ *
+ * If all discussions are expanded, it will collapse all of them
+ * If all discussions are collapsed, it will expand all of them
+ * If some discussions are open and others closed, it will expand the closed ones.
+ *
+ * @param {Object} diff
+ */
+export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+ const shouldCloseAll = getters.diffHasAllExpandedDiscussions(diff);
+ const shouldExpandAll = getters.diffHasAllCollpasedDiscussions(diff);
+
+ discussions.forEach(discussion => {
+ const data = { discussionId: discussion.id };
+
+ if (shouldCloseAll) {
+ dispatch('collapseDiscussion', data, { root: true });
+ } else if (shouldExpandAll || (!shouldCloseAll && !shouldExpandAll && !discussion.expanded)) {
+ dispatch('expandDiscussion', data, { root: true });
+ }
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index f3c2d7427e7..f89acb73ed8 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,3 +1,4 @@
+import _ from 'underscore';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
@@ -8,5 +9,52 @@ export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
+/**
+ * Checks if the diff has all discussions expanded
+ * @param {Object} diff
+ * @returns {Boolean}
+ */
+export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+
+ return (discussions.length && discussions.every(discussion => discussion.expanded)) || false;
+};
+
+/**
+ * Checks if the diff has all discussions collpased
+ * @param {Object} diff
+ * @returns {Boolean}
+ */
+export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+
+ return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false;
+};
+
+/**
+ * Checks if the diff has any open discussions
+ * @param {Object} diff
+ * @returns {Boolean}
+ */
+export const diffHasExpandedDiscussions = (state, getters) => diff => {
+ const discussions = getters.getDiffFileDiscussions(diff);
+
+ return (
+ (discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) ||
+ false
+ );
+};
+
+/**
+ * Returns an array with the discussions of the given diff
+ * @param {Object} diff
+ * @returns {Array}
+ */
+export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => diff =>
+ rootGetters.discussions.filter(
+ discussion =>
+ discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
+ ) || [];
+
+// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export default () => {};
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 9f016e0338f..257a7432c20 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,6 +1,7 @@
<script>
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
+import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
@@ -13,6 +14,7 @@ const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
+ NewModal,
IdeSidebar,
RepoTabs,
IdeStatusBar,
@@ -137,5 +139,6 @@ export default {
/>
</div>
<ide-status-bar :file="activeFile"/>
+ <new-modal />
</article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 0582ad32e92..715dc1bfb42 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
+import { rightSidebarViews } from '../constants';
export default {
components: {
@@ -49,6 +50,7 @@ export default {
this.stopPipelinePolling();
},
methods: {
+ ...mapActions(['setRightPane']),
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() {
this.intervalId = setInterval(() => {
@@ -69,24 +71,31 @@ export default {
return `${this.currentProject.web_url}/commit/${shortSha}`;
},
},
+ rightSidebarViews,
};
</script>
<template>
<footer class="ide-status-bar">
<div
- v-if="lastCommit && lastCommitFormatedAge"
+ v-if="lastCommit"
class="ide-status-branch"
>
<span
v-if="latestPipeline && latestPipeline.details"
class="ide-status-pipeline"
>
- <ci-icon
- v-tooltip
- :status="latestPipeline.details.status"
- :title="latestPipeline.details.status.text"
- />
+ <button
+ type="button"
+ class="p-0 border-0 h-50"
+ @click="setRightPane($options.rightSidebarViews.pipelines)"
+ >
+ <ci-icon
+ v-tooltip
+ :status="latestPipeline.details.status"
+ :title="latestPipeline.details.status.text"
+ />
+ </button>
Pipeline
<a
:href="latestPipeline.details.status.details_path"
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 8fc4ebe6ca6..0a95c0bb30d 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -1,12 +1,16 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import NewDropdown from './new_dropdown/index.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import IdeTreeList from './ide_tree_list.vue';
+import Upload from './new_dropdown/upload.vue';
+import NewEntryButton from './new_dropdown/button.vue';
export default {
components: {
- NewDropdown,
+ Icon,
+ Upload,
IdeTreeList,
+ NewEntryButton,
},
computed: {
...mapState(['currentBranchId']),
@@ -20,23 +24,42 @@ export default {
}
},
methods: {
- ...mapActions(['updateViewer']),
+ ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']),
},
};
</script>
<template>
<ide-tree-list
+ header-class="d-flex w-100"
viewer-type="editor"
>
<template
slot="header"
>
{{ __('Edit') }}
- <new-dropdown
- :project-id="currentProject.name_with_namespace"
- :branch="currentBranchId"
- />
+ <div class="ml-auto d-flex">
+ <new-entry-button
+ :label="__('New file')"
+ :show-label="false"
+ class="d-flex border-0 p-0 mr-3"
+ icon="doc-new"
+ @click="openNewEntryModal({ type: 'blob' })"
+ />
+ <upload
+ :show-label="false"
+ class="d-flex mr-3"
+ button-css-classes="border-0 p-0"
+ @create="createTempEntry"
+ />
+ <new-entry-button
+ :label="__('New directory')"
+ :show-label="false"
+ class="d-flex border-0 p-0"
+ icon="folder-new"
+ @click="openNewEntryModal({ type: 'tree' })"
+ />
+ </div>
</template>
</ide-tree-list>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue
new file mode 100644
index 00000000000..7682b34ce4d
--- /dev/null
+++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue
@@ -0,0 +1,51 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ icon: {
+ type: String,
+ required: true,
+ },
+ iconClasses: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ showLabel: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ methods: {
+ clicked() {
+ this.$emit('click');
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ :aria-label="label"
+ type="button"
+ @click.stop.prevent="clicked"
+ >
+ <icon
+ :name="icon"
+ :css-classes="iconClasses"
+ />
+ <template v-if="showLabel">
+ {{ label }}
+ </template>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 1e398d7e1aa..c29e49ba766 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -3,12 +3,14 @@ import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
+import ItemButton from './button.vue';
export default {
components: {
icon,
newModal,
upload,
+ ItemButton,
},
props: {
branch: {
@@ -20,11 +22,13 @@ export default {
required: false,
default: '',
},
+ mouseOver: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
- openModal: false,
- modalType: '',
dropdownOpen: false,
};
},
@@ -34,17 +38,18 @@ export default {
this.$refs.dropdownMenu.scrollIntoView();
});
},
+ mouseOver() {
+ if (!this.mouseOver) {
+ this.dropdownOpen = false;
+ }
+ },
},
methods: {
- ...mapActions(['createTempEntry']),
+ ...mapActions(['createTempEntry', 'openNewEntryModal']),
createNewItem(type) {
- this.modalType = type;
- this.openModal = true;
+ this.openNewEntryModal({ type, path: this.path });
this.dropdownOpen = false;
},
- hideModal() {
- this.openModal = false;
- },
openDropdown() {
this.dropdownOpen = !this.dropdownOpen;
},
@@ -58,23 +63,19 @@ export default {
:class="{
show: dropdownOpen,
}"
- class="dropdown"
+ class="dropdown d-flex"
>
<button
+ :aria-label="__('Create new file or directory')"
type="button"
- class="btn btn-sm btn-default dropdown-toggle add-to-tree"
- aria-label="Create new file or directory"
+ class="rounded border-0 d-flex ide-entry-dropdown-toggle"
@click.stop="openDropdown()"
>
<icon
- :size="12"
- name="plus"
- css-classes="float-left"
+ name="hamburger"
/>
<icon
- :size="12"
name="arrow-down"
- css-classes="float-left"
/>
</button>
<ul
@@ -82,39 +83,30 @@ export default {
class="dropdown-menu dropdown-menu-right"
>
<li>
- <a
- href="#"
- role="button"
- @click.stop.prevent="createNewItem('blob')"
- >
- {{ __('New file') }}
- </a>
+ <item-button
+ :label="__('New file')"
+ class="d-flex"
+ icon="doc-new"
+ icon-classes="mr-2"
+ @click="createNewItem('blob')"
+ />
</li>
<li>
<upload
- :branch-id="branch"
:path="path"
@create="createTempEntry"
/>
</li>
<li>
- <a
- href="#"
- role="button"
- @click.stop.prevent="createNewItem('tree')"
- >
- {{ __('New directory') }}
- </a>
+ <item-button
+ :label="__('New directory')"
+ class="d-flex"
+ icon="folder-new"
+ icon-classes="mr-2"
+ @click="createNewItem('tree')"
+ />
</li>
</ul>
</div>
- <new-modal
- v-if="openModal"
- :type="modalType"
- :branch-id="branch"
- :path="path"
- @hide="hideModal"
- @create="createTempEntry"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 1e9668d5154..1867b7980d2 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,78 +1,70 @@
<script>
import { __ } from '~/locale';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { mapActions, mapState } from 'vuex';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
export default {
components: {
- DeprecatedModal,
- },
- props: {
- branchId: {
- type: String,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
+ GlModal,
},
data() {
return {
- entryName: this.path !== '' ? `${this.path}/` : '',
+ name: '',
};
},
computed: {
+ ...mapState(['newEntryModal']),
+ entryName: {
+ get() {
+ return this.name || (this.newEntryModal.path !== '' ? `${this.newEntryModal.path}/` : '');
+ },
+ set(val) {
+ this.name = val;
+ },
+ },
modalTitle() {
- if (this.type === 'tree') {
+ if (this.newEntryModal.type === 'tree') {
return __('Create new directory');
}
return __('Create new file');
},
buttonLabel() {
- if (this.type === 'tree') {
+ if (this.newEntryModal.type === 'tree') {
return __('Create directory');
}
return __('Create file');
},
},
- mounted() {
- this.$refs.fieldName.focus();
- },
methods: {
+ ...mapActions(['createTempEntry']),
createEntryInStore() {
- this.$emit('create', {
- branchId: this.branchId,
- name: this.entryName,
- type: this.type,
+ this.createTempEntry({
+ name: this.name,
+ type: this.newEntryModal.type,
});
-
- this.hideModal();
},
- hideModal() {
- this.$emit('hide');
+ focusInput() {
+ setTimeout(() => {
+ this.$refs.fieldName.focus();
+ });
},
},
};
</script>
<template>
- <deprecated-modal
- :title="modalTitle"
- :primary-button-label="buttonLabel"
- kind="success"
- @cancel="hideModal"
+ <gl-modal
+ id="ide-new-entry"
+ :header-title-text="modalTitle"
+ :footer-primary-button-text="buttonLabel"
+ footer-primary-button-variant="success"
@submit="createEntryInStore"
+ @open="focusInput"
>
- <form
- slot="body"
+ <div
class="form-group row"
- @submit.prevent="createEntryInStore"
>
<label class="label-light col-form-label col-sm-3">
{{ __('Name') }}
@@ -85,6 +77,6 @@ export default {
class="form-control"
/>
</div>
- </form>
- </deprecated-modal>
+ </div>
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 677b282bd61..5b1743bb30e 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -1,71 +1,85 @@
<script>
- export default {
- props: {
- branchId: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: false,
- default: '',
- },
+import Icon from '~/vue_shared/components/icon.vue';
+import ItemButton from './button.vue';
+
+export default {
+ components: {
+ Icon,
+ ItemButton,
+ },
+ props: {
+ path: {
+ type: String,
+ required: false,
+ default: '',
},
- mounted() {
- this.$refs.fileUpload.addEventListener('change', this.openFile);
+ showLabel: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- beforeDestroy() {
- this.$refs.fileUpload.removeEventListener('change', this.openFile);
+ buttonCssClasses: {
+ type: String,
+ required: false,
+ default: null,
},
- methods: {
- createFile(target, file, isText) {
- const { name } = file;
- let { result } = target;
+ },
+ mounted() {
+ this.$refs.fileUpload.addEventListener('change', this.openFile);
+ },
+ beforeDestroy() {
+ this.$refs.fileUpload.removeEventListener('change', this.openFile);
+ },
+ methods: {
+ createFile(target, file, isText) {
+ const { name } = file;
+ let { result } = target;
- if (!isText) {
- // eslint-disable-next-line prefer-destructuring
- result = result.split('base64,')[1];
- }
+ if (!isText) {
+ // eslint-disable-next-line prefer-destructuring
+ result = result.split('base64,')[1];
+ }
- this.$emit('create', {
- name: `${(this.path ? `${this.path}/` : '')}${name}`,
- branchId: this.branchId,
- type: 'blob',
- content: result,
- base64: !isText,
- });
- },
- readFile(file) {
- const reader = new FileReader();
- const isText = file.type.match(/text.*/) !== null;
+ this.$emit('create', {
+ name: `${this.path ? `${this.path}/` : ''}${name}`,
+ type: 'blob',
+ content: result,
+ base64: !isText,
+ });
+ },
+ readFile(file) {
+ const reader = new FileReader();
+ const isText = file.type.match(/text.*/) !== null;
- reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
+ reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
- if (isText) {
- reader.readAsText(file);
- } else {
- reader.readAsDataURL(file);
- }
- },
- openFile() {
- Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
- },
- startFileUpload() {
- this.$refs.fileUpload.click();
- },
+ if (isText) {
+ reader.readAsText(file);
+ } else {
+ reader.readAsDataURL(file);
+ }
+ },
+ openFile() {
+ Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
- };
+ startFileUpload() {
+ this.$refs.fileUpload.click();
+ },
+ },
+};
</script>
<template>
<div>
- <a
- href="#"
- role="button"
- @click.stop.prevent="startFileUpload"
- >
- {{ __('Upload file') }}
- </a>
+ <item-button
+ :class="buttonCssClasses"
+ :show-label="showLabel"
+ :icon-classes="showLabel ? 'mr-2' : ''"
+ :label="__('Upload file')"
+ class="d-flex"
+ icon="upload"
+ @click="startFileUpload"
+ />
<input
id="file-upload"
ref="fileUpload"
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index f490a3a2a39..3b4dd5ae9aa 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -40,6 +40,11 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ mouseOver: false,
+ };
+ },
computed: {
...mapGetters([
'getChangesInFolder',
@@ -142,6 +147,9 @@ export default {
hasUrlAtCurrentRoute() {
return this.$router.currentRoute.path === `/project${this.file.url}`;
},
+ toggleHover(over) {
+ this.mouseOver = over;
+ },
},
};
</script>
@@ -153,6 +161,8 @@ export default {
class="file"
role="button"
@click="clickFile"
+ @mouseover="toggleHover(true)"
+ @mouseout="toggleHover(false)"
>
<div
class="file-name"
@@ -206,6 +216,7 @@ export default {
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
+ :mouse-over="mouseOver"
class="float-right prepend-left-8"
/>
</div>
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 5e91fa915ff..b5bd6f5a6bb 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -52,7 +52,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
export const createTempEntry = (
{ state, commit, dispatch },
- { branchId, name, type, content = '', base64 = false },
+ { name, type, content = '', base64 = false },
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
@@ -81,7 +81,7 @@ export const createTempEntry = (
commit(types.CREATE_TMP_ENTRY, {
data,
projectId: state.currentProjectId,
- branchId,
+ branchId: state.currentBranchId,
});
if (type === 'blob') {
@@ -100,7 +100,7 @@ export const createTempEntry = (
worker.postMessage({
data: [fullName],
projectId: state.currentProjectId,
- branchId,
+ branchId: state.currentBranchId,
type,
tempFile: true,
base64,
@@ -178,6 +178,13 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
export const setErrorMessage = ({ commit }, errorMessage) =>
commit(types.SET_ERROR_MESSAGE, errorMessage);
+export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
+ commit(types.OPEN_NEW_ENTRY_MODAL, { type, path });
+
+ // open the modal manually so we don't mess around with dropdown/rows
+ $('#ide-new-entry').modal('show');
+};
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
index cdd8076952f..6ef938b0ae2 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -31,7 +31,7 @@ export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, searc
dispatch('requestMergeRequests', type);
dispatch('resetMergeRequests', type);
- Api.mergeRequests({ scope, state, search })
+ return Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
.catch(() => dispatch('receiveMergeRequestsError', { type, search }));
};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 8cb01f25223..3e67b222e66 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -102,7 +102,7 @@ export const receiveJobsSuccess = ({ commit }, { id, data }) =>
export const fetchJobs = ({ dispatch }, stage) => {
dispatch('requestJobs', stage.id);
- axios
+ return axios
.get(stage.dropdownPath)
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
.catch(() => dispatch('receiveJobsError', stage));
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 555802e1811..8d6f9ccaf34 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -74,3 +74,5 @@ export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
+
+export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 702be2140e2..f8091f5b5e0 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -166,6 +166,11 @@ export default {
[types.SET_ERROR_MESSAGE](state, errorMessage) {
Object.assign(state, { errorMessage });
},
+ [types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) {
+ Object.assign(state, {
+ newEntryModal: { type, path },
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index be229b2c723..0f32a267469 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -26,4 +26,8 @@ export default () => ({
rightPane: null,
links: {},
errorMessage: null,
+ newEntryModal: {
+ type: '',
+ path: '',
+ },
});
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index b2bf86eea56..3eefbe11c37 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -15,6 +15,8 @@ let eTagPoll;
export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data);
+export const collapseDiscussion = ({ commit }, data) => commit(types.COLLAPSE_DISCUSSION, data);
+
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index a25098fbc06..6f374f78691 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -1,7 +1,6 @@
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE';
-export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
@@ -11,12 +10,16 @@ export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
-export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
+// DISCUSSION
+export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
+export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
+export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
+
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index a1849269010..ab6a95e2601 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -58,6 +58,11 @@ export default {
discussion.expanded = true;
},
+ [types.COLLAPSE_DISCUSSION](state, { discussionId }) {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ discussion.expanded = false;
+ },
+
[types.REMOVE_PLACEHOLDER_NOTES](state) {
const { discussions } = state;
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 9aa83ce6269..ff19b9a9c30 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -39,7 +39,6 @@ export default class Todos {
}
initFilters() {
- this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
@@ -54,16 +53,7 @@ export default class Todos {
filterable: searchFields ? true : false,
search: { fields: searchFields },
data: $dropdown.data('data'),
- clicked: () => {
- const $formEl = $dropdown.closest('form.filter-form');
- const mutexDropdowns = {
- group_id: 'project_id',
- project_id: 'group_id',
- };
-
- $formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove();
- $formEl.submit();
- },
+ clicked: () => $dropdown.closest('form.filter-form').submit(),
});
}
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
deleted file mode 100644
index ffaed9c7193..00000000000
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<script>
-import { __ } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
-
-import Icon from '~/vue_shared/components/icon.vue';
-import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
-
-const MARK_TEXT = __('Mark todo as done');
-const TODO_TEXT = __('Add todo');
-
-export default {
- directives: {
- tooltip,
- },
- components: {
- Icon,
- LoadingIcon,
- },
- props: {
- issuableId: {
- type: Number,
- required: true,
- },
- issuableType: {
- type: String,
- required: true,
- },
- isTodo: {
- type: Boolean,
- required: false,
- default: true,
- },
- isActionActive: {
- type: Boolean,
- required: false,
- default: false,
- },
- collapsed: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- buttonClasses() {
- return this.collapsed ?
- 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
- 'btn btn-default btn-todo issuable-header-btn float-right';
- },
- buttonLabel() {
- return this.isTodo ? MARK_TEXT : TODO_TEXT;
- },
- collapsedButtonIconClasses() {
- return this.isTodo ? 'todo-undone' : '';
- },
- collapsedButtonIcon() {
- return this.isTodo ? 'todo-done' : 'todo-add';
- },
- },
- methods: {
- handleButtonClick() {
- this.$emit('toggleTodo');
- },
- },
-};
-</script>
-
-<template>
- <button
- v-tooltip
- :class="buttonClasses"
- :title="buttonLabel"
- :aria-label="buttonLabel"
- :data-issuable-id="issuableId"
- :data-issuable-type="issuableType"
- type="button"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
- @click="handleButtonClick"
- >
- <icon
- v-show="collapsed"
- :css-classes="collapsedButtonIconClasses"
- :name="collapsedButtonIcon"
- />
- <span
- v-show="!collapsed"
- class="issuable-todo-inner"
- >
- {{ buttonLabel }}
- </span>
- <loading-icon
- v-show="isActionActive"
- :inline="true"
- />
- </button>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 55b87f3a8ec..9aff95dcfec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -32,7 +32,7 @@
};
</script>
<template>
- <div class="space-children d-flex append-right-10">
+ <div class="space-children d-flex append-right-10 widget-status-icon">
<div
v-if="isLoading"
class="mr-widget-icon"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index fe777a07189..a5ca7b719a1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -233,7 +233,7 @@ export default {
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
- <span class="btn-group append-bottom-5">
+ <span class="btn-group">
<button
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index b298b989203..416eda796a7 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -45,6 +45,11 @@ export default {
emitSubmit(event) {
this.$emit('submit', event);
},
+ opened({ propertyName }) {
+ if (propertyName === 'opacity') {
+ this.$emit('open');
+ }
+ },
},
};
</script>
@@ -55,6 +60,7 @@ export default {
class="modal fade"
tabindex="-1"
role="dialog"
+ @transitionend="opened"
>
<div
:class="modalSizeClass"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index 80dc7d3557c..ac2e99abe77 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -12,11 +12,6 @@ export default {
type: Boolean,
required: true,
},
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
tooltipLabel() {
@@ -35,12 +30,10 @@ export default {
<button
v-tooltip
:title="tooltipLabel"
- :class="cssClasses"
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
data-container="body"
data-placement="left"
- data-boundary="viewport"
@click="toggle"
>
<i
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 9dbb04e5443..8bab8cf36b1 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -321,11 +321,18 @@
}
&.activities {
+ display: flex;
border-bottom: 1px solid $border-color;
+ overflow: hidden;
.nav-links {
border-bottom: 0;
}
+
+ @include media-breakpoint-down(xs) {
+ display: block;
+ overflow: visible;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 7e89f8998fb..5e39bbb9890 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -518,6 +518,12 @@
outline: none;
color: $gl-link-hover-color;
}
+
+ .caret-icon {
+ position: relative;
+ top: 2px;
+ left: -1px;
+ }
}
// Mobile
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index f6617380cc0..f9fd9f1ab8b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -449,7 +449,6 @@
.todo-undone {
color: $gl-link-color;
- fill: $gl-link-color;
}
.author {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index c32049e1b33..5835b8b8c9b 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -116,10 +116,8 @@
.modify-merge-commit-link {
padding: 0;
-
background-color: transparent;
border: 0;
-
color: $gl-text-color;
&:hover,
@@ -216,6 +214,10 @@
}
}
+ .widget-status-icon {
+ align-self: flex-start;
+ }
+
.mr-widget-body {
line-height: 28px;
@@ -501,10 +503,6 @@
}
}
-.merge-request-details .content-block {
- border-bottom: 0;
-}
-
.mr-source-target {
display: flex;
flex-wrap: wrap;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 6e2b285285a..8b1227b9131 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -44,6 +44,7 @@
padding-bottom: $grid-size;
.file {
+ height: 32px;
cursor: pointer;
&.file-active {
@@ -716,32 +717,6 @@
justify-content: center;
}
-.ide-new-btn {
- .btn {
- padding-top: 3px;
- padding-bottom: 3px;
- }
-
- .dropdown {
- display: flex;
- }
-
- .dropdown-toggle svg {
- top: 0;
- }
-
- .dropdown-menu {
- left: auto;
- right: 0;
-
- label {
- font-weight: $gl-font-weight-normal;
- padding: 5px 8px;
- margin-bottom: 0;
- }
- }
-}
-
.ide {
overflow: hidden;
@@ -1340,3 +1315,24 @@
overflow: auto;
}
}
+
+.ide-entry-dropdown-toggle {
+ padding: $gl-padding-4;
+ background-color: $theme-gray-100;
+
+ &:hover {
+ background-color: $theme-gray-200;
+ }
+
+ &:active,
+ &:focus {
+ color: $white-normal;
+ background-color: $blue-500;
+ outline: 0;
+ }
+}
+
+.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
+ color: $white-normal;
+ background-color: $blue-500;
+}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 010a2c05a1c..e5d7dd13915 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -174,18 +174,6 @@
}
}
-@include media-breakpoint-down(lg) {
- .todos-filters {
- .filter-categories {
- width: 75%;
-
- .filter-item {
- margin-bottom: 10px;
- }
- }
- }
-}
-
@include media-breakpoint-down(xs) {
.todo {
.avatar {
@@ -211,10 +199,6 @@
}
.todos-filters {
- .filter-categories {
- width: auto;
- }
-
.dropdown-menu-toggle {
width: 100%;
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 21cc6dfdd16..f45fcd4d900 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -30,7 +30,13 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
helper_method :can?
- helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
+ helper_method :import_sources_enabled?, :github_import_enabled?,
+ :gitea_import_enabled?, :github_import_configured?,
+ :gitlab_import_enabled?, :gitlab_import_configured?,
+ :bitbucket_import_enabled?, :bitbucket_import_configured?,
+ :google_code_import_enabled?, :fogbugz_import_enabled?,
+ :git_import_enabled?, :gitlab_project_import_enabled?,
+ :manifest_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -351,6 +357,10 @@ class ApplicationController < ActionController::Base
Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
end
+ def manifest_import_enabled?
+ Group.supports_nested_groups? && Gitlab::CurrentSettings.import_sources.include?('manifest')
+ end
+
# U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html
diff --git a/app/controllers/concerns/todos_actions.rb b/app/controllers/concerns/todos_actions.rb
deleted file mode 100644
index c0acdb3498d..00000000000
--- a/app/controllers/concerns/todos_actions.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module TodosActions
- extend ActiveSupport::Concern
-
- def create
- todo = TodoService.new.mark_todo(issuable, current_user)
-
- render json: {
- count: TodosFinder.new(current_user, state: :pending).execute.count,
- delete_path: dashboard_todo_path(todo)
- }
- end
-end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index bd7111e28bc..f9e8fe624e8 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def todo_params
- params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
+ params.permit(:action_id, :author_id, :project_id, :type, :sort, :state)
end
def redirect_out_of_range(todos)
diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb
new file mode 100644
index 00000000000..e5a719fa0df
--- /dev/null
+++ b/app/controllers/import/manifest_controller.rb
@@ -0,0 +1,93 @@
+class Import::ManifestController < Import::BaseController
+ before_action :whitelist_query_limiting, only: [:create]
+ before_action :verify_import_enabled
+ before_action :ensure_import_vars, only: [:create, :status]
+
+ def new
+ end
+
+ def status
+ @already_added_projects = find_already_added_projects
+ already_added_import_urls = @already_added_projects.pluck(:import_url)
+
+ @pending_repositories = repositories.to_a.reject do |repository|
+ already_added_import_urls.include?(repository[:url])
+ end
+ end
+
+ def upload
+ group = Group.find(params[:group_id])
+
+ unless can?(current_user, :create_projects, group)
+ @errors = ["You don't have enough permissions to create projects in the selected group"]
+
+ render :new && return
+ end
+
+ manifest = Gitlab::ManifestImport::Manifest.new(params[:manifest].tempfile)
+
+ if manifest.valid?
+ session[:manifest_import_repositories] = manifest.projects
+ session[:manifest_import_group_id] = group.id
+
+ redirect_to status_import_manifest_path
+ else
+ @errors = manifest.errors
+
+ render :new
+ end
+ end
+
+ def jobs
+ render json: find_jobs
+ end
+
+ def create
+ repository = repositories.find do |project|
+ project[:id] == params[:repo_id].to_i
+ end
+
+ project = Gitlab::ManifestImport::ProjectCreator.new(repository, group, current_user).execute
+
+ if project.persisted?
+ render json: ProjectSerializer.new.represent(project)
+ else
+ render json: { errors: project_save_error(project) }, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ def ensure_import_vars
+ unless group && repositories.present?
+ redirect_to(new_import_manifest_path)
+ end
+ end
+
+ def group
+ @group ||= Group.find_by(id: session[:manifest_import_group_id])
+ end
+
+ def repositories
+ @repositories ||= session[:manifest_import_repositories]
+ end
+
+ def find_jobs
+ find_already_added_projects.to_json(only: [:id], methods: [:import_status])
+ end
+
+ def find_already_added_projects
+ group.all_projects
+ .where(import_type: 'manifest')
+ .where(creator_id: current_user)
+ .includes(:import_state)
+ end
+
+ def verify_import_enabled
+ render_404 unless manifest_import_enabled?
+ end
+
+ def whitelist_query_limiting
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/48939')
+ end
+end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 93fb9da6510..a41fcb85c40 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -1,13 +1,19 @@
class Projects::TodosController < Projects::ApplicationController
- include Gitlab::Utils::StrongMemoize
- include TodosActions
-
before_action :authenticate_user!, only: [:create]
+ def create
+ todo = TodoService.new.mark_todo(issuable, current_user)
+
+ render json: {
+ count: TodosFinder.new(current_user, state: :pending).execute.count,
+ delete_path: dashboard_todo_path(todo)
+ }
+ end
+
private
def issuable
- strong_memoize(:issuable) do
+ @issuable ||= begin
case params[:issuable_type]
when "issue"
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 2156413fb26..09e2c586f2a 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -15,7 +15,6 @@
class TodosFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
- include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? }
@@ -35,11 +34,9 @@ class TodosFinder
items = by_author(items)
items = by_state(items)
items = by_type(items)
- items = by_group(items)
# Filtering by project HAS TO be the last because we use
# the project IDs yielded by the todos query thus far
items = by_project(items)
- items = visible_to_user(items)
sort(items)
end
@@ -85,10 +82,6 @@ class TodosFinder
params[:project_id].present?
end
- def group?
- params[:group_id].present?
- end
-
def project
return @project if defined?(@project)
@@ -107,14 +100,18 @@ class TodosFinder
@project
end
- def group
- strong_memoize(:group) do
- Group.find(params[:group_id])
+ def project_ids(items)
+ ids = items.except(:order).select(:project_id)
+ if Gitlab::Database.mysql?
+ # To make UPDATE work on MySQL, wrap it in a SELECT with an alias
+ ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t")
end
+
+ ids
end
def type?
- type.present? && %w(Issue MergeRequest Epic).include?(type)
+ type.present? && %w(Issue MergeRequest).include?(type)
end
def type
@@ -151,37 +148,12 @@ class TodosFinder
def by_project(items)
if project?
- items = items.where(project: project)
- end
-
- items
- end
+ items.where(project: project)
+ else
+ projects = Project.public_or_visible_to_user(current_user)
- def by_group(items)
- if group?
- groups = group.self_and_descendants
- items = items.where(
- 'project_id IN (?) OR group_id IN (?)',
- Project.where(group: groups).select(:id),
- groups.select(:id)
- )
+ items.joins(:project).merge(projects)
end
-
- items
- end
-
- def visible_to_user(items)
- projects = Project.public_or_visible_to_user(current_user)
- groups = Group.public_or_visible_to_user(current_user)
-
- items
- .joins('LEFT JOIN namespaces ON namespaces.id = todos.group_id')
- .joins('LEFT JOIN projects ON projects.id = todos.project_id')
- .where(
- 'project_id IN (?) OR group_id IN (?)',
- projects.select(:id),
- groups.select(:id)
- )
end
def by_state(items)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 7bbdc798ddd..8766bb43cac 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -131,19 +131,6 @@ module IssuablesHelper
end
end
- def group_dropdown_label(group_id, default_label)
- return default_label if group_id.nil?
- return "Any group" if group_id == "0"
-
- group = ::Group.find_by(id: group_id)
-
- if group
- group.full_name
- else
- default_label
- end
- end
-
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
title =
case milestone_title
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 9be93fa69ae..9008db1b300 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -3,7 +3,7 @@ module NamespacesHelper
params.dig(:project, :namespace_id) || params[:namespace_id]
end
- def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
+ def namespaces_options(selected = :current_user, display_path: false, extra_group: nil, groups_only: false)
groups = current_user.manageable_groups
.joins(:route)
.includes(:route)
@@ -20,10 +20,13 @@ module NamespacesHelper
options = []
options << options_for_group(groups, display_path: display_path, type: 'group')
- options << options_for_group(users, display_path: display_path, type: 'user')
- if selected == :current_user && current_user.namespace
- selected = current_user.namespace.id
+ unless groups_only
+ options << options_for_group(users, display_path: display_path, type: 'user')
+
+ if selected == :current_user && current_user.namespace
+ selected = current_user.namespace.id
+ end
end
grouped_options_for_select(options, selected)
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 7cd74358168..f7620e0b6b8 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -43,7 +43,7 @@ module TodosHelper
project_commit_path(todo.project,
todo.target, anchor: anchor)
else
- path = [todo.parent, todo.target]
+ path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
path.unshift(:pipelines) if todo.build_failed?
@@ -167,12 +167,4 @@ module TodosHelper
def show_todo_state?(todo)
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end
-
- def todo_group_options
- groups = current_user.authorized_groups.map do |group|
- { id: group.id, text: group.full_name }
- end
-
- groups.unshift({ id: '', text: 'Any Group' }).to_json
- end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d8ddb4bc667..db86400128c 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -653,6 +653,7 @@ module Ci
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
+ variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 7a459078151..b93c1145f82 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -243,12 +243,6 @@ module Issuable
opened?
end
- def overdue?
- return false unless respond_to?(:due_date)
-
- due_date.try(:past?) || false
- end
-
def user_notes_count
if notes.loaded?
# Use the in-memory association to select and count to avoid hitting the db
diff --git a/app/models/group.rb b/app/models/group.rb
index 28677320e28..ddebaff50b0 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -39,8 +39,6 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
- has_many :todos
-
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
@@ -84,12 +82,6 @@ class Group < Namespace
where(id: user.authorized_groups.select(:id).reorder(nil))
end
- def public_or_visible_to_user(user)
- where('id IN (?) OR namespaces.visibility_level IN (?)',
- user.authorized_groups.select(:id),
- Gitlab::VisibilityLevel.levels_for_user(user))
- end
-
def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 983684a5e05..4715d942c8d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -275,6 +275,10 @@ class Issue < ActiveRecord::Base
user ? readable_by?(user) : publicly_visible?
end
+ def overdue?
+ due_date.try(:past?) || false
+ end
+
def check_for_spam?
project.public? && (title_changed? || description_changed?)
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 3918bbee194..abc40d9016e 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -229,10 +229,6 @@ class Note < ActiveRecord::Base
!for_personal_snippet?
end
- def for_issuable?
- for_issue? || for_merge_request?
- end
-
def skip_project_check?
!for_project_noteable?
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 9195408551f..1933c46ee44 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -32,6 +32,7 @@ class NotificationSetting < ActiveRecord::Base
:reopen_issue,
:close_issue,
:reassign_issue,
+ :issue_due,
:new_merge_request,
:push_to_merge_request,
:reopen_merge_request,
diff --git a/app/models/project.rb b/app/models/project.rb
index 1894de6ceed..e29bca365a4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -368,8 +368,10 @@ class Project < ActiveRecord::Base
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
validates :build_timeout, allow_nil: true,
- numericality: { greater_than_or_equal_to: 600,
- message: 'needs to be at least 10 minutes' }
+ numericality: { greater_than_or_equal_to: 10.minutes,
+ less_than: 1.month,
+ only_integer: true,
+ message: 'needs to be beetween 10 minutes and 1 month' }
# Returns a collection of projects that is either public or visible to the
# logged in user.
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index fc868c3ebb7..9ae2fb0013a 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -20,7 +20,6 @@ class ProjectWiki
@user = user
end
- delegate :empty?, to: :pages
delegate :repository_storage, :hashed_storage?, to: :project
def path
@@ -74,6 +73,10 @@ class ProjectWiki
!!find_page('home')
end
+ def empty?
+ pages(limit: 1).empty?
+ end
+
# Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages.
def pages(limit: nil)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5ed2a7b4068..a96c73e6ab7 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -174,8 +174,8 @@ class Repository
CommitCollection.new(project, commits, ref)
end
- def find_branch(name, fresh_repo: true)
- raw_repository.find_branch(name, fresh_repo)
+ def find_branch(name)
+ raw_repository.find_branch(name)
end
def find_tag(name)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 942cbb754e3..a2ab405fdbe 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -22,18 +22,15 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
- belongs_to :group
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
- validates :action, :target_type, :user, presence: true
+ validates :action, :project, :target_type, :user, presence: true
validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
- validates :project, presence: true, unless: :group_id
- validates :group, presence: true, unless: :project_id
scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) }
@@ -47,7 +44,7 @@ class Todo < ActiveRecord::Base
state :done
end
- after_save :keep_around_commit, if: :commit_id
+ after_save :keep_around_commit
class << self
# Priority sorting isn't displayed in the dropdown, because we don't show
@@ -82,10 +79,6 @@ class Todo < ActiveRecord::Base
end
end
- def parent
- project
- end
-
def unmergeable?
action == UNMERGEABLE
end
diff --git a/app/services/groups/nested_create_service.rb b/app/services/groups/nested_create_service.rb
index 5c337a9faa5..c2dfbac5414 100644
--- a/app/services/groups/nested_create_service.rb
+++ b/app/services/groups/nested_create_service.rb
@@ -1,11 +1,12 @@
module Groups
class NestedCreateService < Groups::BaseService
- attr_reader :group_path
+ attr_reader :group_path, :visibility_level
def initialize(user, params)
@current_user, @params = user, params.dup
-
@group_path = @params.delete(:group_path)
+ @visibility_level = @params.delete(:visibility_level) ||
+ Gitlab::CurrentSettings.current_application_settings.default_group_visibility
end
def execute
@@ -36,11 +37,12 @@ module Groups
new_params = params.reverse_merge(
path: subgroup_name,
name: subgroup_name,
- parent: last_group
+ parent: last_group,
+ visibility_level: visibility_level
)
- new_params[:visibility_level] ||= Gitlab::CurrentSettings.current_application_settings.default_group_visibility
- last_group = namespace_or_group(partial_path) || Groups::CreateService.new(current_user, new_params).execute
+ last_group = namespace_or_group(partial_path) ||
+ Groups::CreateService.new(current_user, new_params).execute
end
last_group
diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb
index 51ff9eff5e4..c237d2ae8c9 100644
--- a/app/services/metrics_service.rb
+++ b/app/services/metrics_service.rb
@@ -1,35 +1,16 @@
require 'prometheus/client/formats/text'
class MetricsService
- CHECKS = [
- Gitlab::HealthChecks::DbCheck,
- Gitlab::HealthChecks::Redis::RedisCheck,
- Gitlab::HealthChecks::Redis::CacheCheck,
- Gitlab::HealthChecks::Redis::QueuesCheck,
- Gitlab::HealthChecks::Redis::SharedStateCheck,
- Gitlab::HealthChecks::GitalyCheck
- ].freeze
-
def prometheus_metrics_text
Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path)
end
- def health_metrics_text
- metrics = CHECKS.flat_map(&:metrics)
-
- formatter.marshal(metrics)
- end
-
def metrics_text
- prometheus_metrics_text.concat(health_metrics_text)
+ prometheus_metrics_text
end
private
- def formatter
- @formatter ||= Gitlab::HealthChecks::PrometheusTextFormat.new
- end
-
def multiprocess_metrics_path
::Prometheus::Client.configuration.multiprocess_files_dir
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 46f12086555..f91cd03bf5c 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -260,15 +260,15 @@ class TodoService
end
end
- def create_mention_todos(parent, target, author, note = nil, skip_users = [])
+ def create_mention_todos(project, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users
- directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
- attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
+ directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
+ attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
create_todos(directly_addressed_users, attributes)
# Create Todos for mentioned users
- mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users)
- attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
+ mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
+ attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
end
@@ -299,36 +299,36 @@ class TodoService
def attributes_for_todo(project, target, author, action, note = nil)
attributes_for_target(target).merge!(
- project_id: project&.id,
+ project_id: project.id,
author_id: author.id,
action: action,
note: note
)
end
- def filter_todo_users(users, parent, target)
- reject_users_without_access(users, parent, target).uniq
+ def filter_todo_users(users, project, target)
+ reject_users_without_access(users, project, target).uniq
end
- def filter_mentioned_users(parent, target, author, skip_users = [])
+ def filter_mentioned_users(project, target, author, skip_users = [])
mentioned_users = target.mentioned_users(author) - skip_users
- filter_todo_users(mentioned_users, parent, target)
+ filter_todo_users(mentioned_users, project, target)
end
- def filter_directly_addressed_users(parent, target, author, skip_users = [])
+ def filter_directly_addressed_users(project, target, author, skip_users = [])
directly_addressed_users = target.directly_addressed_users(author) - skip_users
- filter_todo_users(directly_addressed_users, parent, target)
+ filter_todo_users(directly_addressed_users, project, target)
end
- def reject_users_without_access(users, parent, target)
- if target.is_a?(Note) && target.for_issuable?
+ def reject_users_without_access(users, project, target)
+ if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
target = target.noteable
end
if target.is_a?(Issuable)
select_users(users, :"read_#{target.to_ability_name}", target)
else
- select_users(users, :read_project, parent)
+ select_users(users, :read_project, project)
end
end
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index c8008771236..a3773e90cfb 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -6,7 +6,7 @@
= render_if_exists 'admin/namespace_plan', f: f
.form-group.row.group-description-holder
- = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
+ = f.label :avatar, _("Group avatar"), class: 'col-form-label col-sm-2'
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
@@ -26,12 +26,12 @@
.alert.alert-info
= render 'shared/group_tips'
.form-actions
- = f.submit 'Create group', class: "btn btn-create"
- = link_to 'Cancel', admin_groups_path, class: "btn btn-cancel"
+ = f.submit _('Create group'), class: "btn btn-create"
+ = link_to _('Cancel'), admin_groups_path, class: "btn btn-cancel"
- else
.form-actions
- = f.submit 'Save changes', class: "btn btn-save"
- = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
+ = f.submit _('Save changes'), class: "btn btn-save"
+ = link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel"
= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 3f96988c203..0a688b90f3a 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -3,8 +3,8 @@
%li.group-row{ class: css_class }
.controls
- = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
- = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
+ = link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
+ = link_to _('Delete'), [:admin, group], data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } }, method: :delete, class: 'btn btn-remove'
.stats
%span.badge.badge-pill
= storage_counter(group.storage_size)
diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml
index c2b9807015d..8e9e1a58a17 100644
--- a/app/views/admin/groups/edit.html.haml
+++ b/app/views/admin/groups/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", @group.name, "Groups"
-%h3.page-title Edit group: #{@group.name}
+- page_title _("Edit"), @group.name, _("Groups")
+%h3.page-title= _('Edit group: %{group_name}') % { group_name: @group.name }
%hr
= render 'form', visibility_level: @group.visibility_level
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 25946ba6eaf..6a9b85b4109 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Groups"
+- page_title _("Groups")
%div{ class: container_class }
.top-area
@@ -13,7 +13,7 @@
= icon("search", class: "search-icon")
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= link_to new_admin_group_path, class: "btn btn-new" do
- New group
+ = _('New group')
%ul.content-list
= render @groups
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
index 8f9fe96249f..553e8638e52 100644
--- a/app/views/admin/groups/new.html.haml
+++ b/app/views/admin/groups/new.html.haml
@@ -1,4 +1,4 @@
-- page_title "New Group"
-%h3.page-title New group
+- page_title _("New Group")
+%h3.page-title= _('New group')
%hr
= render 'form', visibility_level: default_group_visibility
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index a40f98ad24f..72b068ea6b5 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,61 +1,58 @@
-- add_to_breadcrumbs "Groups", admin_groups_path
+- add_to_breadcrumbs _("Groups"), admin_groups_path
- breadcrumb_title @group.name
-- page_title @group.name, "Groups"
+- page_title @group.name, _("Groups")
%h3.page-title
- Group: #{@group.full_name}
+ = _('Group: %{group_name}') % { group_name: @group.full_name }
= link_to admin_group_edit_path(@group), class: "btn float-right" do
%i.fa.fa-pencil-square-o
- Edit
+ = _('Edit')
%hr
.row
.col-md-6
.card
.card-header
- Group info:
+ = _('Group info:')
%ul.content-list
%li
.avatar-container.s60
= group_icon(@group, class: "avatar s60")
%li
- %span.light Name:
+ %span.light= _('Name:')
%strong= @group.name
%li
- %span.light Path:
+ %span.light= _('Path:')
%strong
= @group.path
%li
- %span.light Description:
+ %span.light= _('Description:')
%strong
= @group.description
%li
- %span.light Visibility level:
+ %span.light= _('Visibility level:')
%strong
= visibility_level_label(@group.visibility_level)
%li
- %span.light Created on:
+ %span.light= _('Created on:')
%strong
= @group.created_at.to_s(:medium)
= render_if_exists 'admin/namespace_plan_info', namespace: @group
%li
- %span.light Storage:
- %strong= storage_counter(@group.storage_size)
- (
- = storage_counter(@group.repository_size)
- repositories,
- = storage_counter(@group.build_artifacts_size)
- build artifacts,
- = storage_counter(@group.lfs_objects_size)
- LFS
- )
+ %span.light= _('Storage:')
+ - counter_storage = storage_counter(@group.storage_size)
+ - counter_repositories = storage_counter(@group.repository_size)
+ - counter_build_artifacts = storage_counter(@group.build_artifacts_size)
+ - counter_lfs_objects = storage_counter(@group.lfs_objects_size)
+ %strong
+ = _("%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)") % { counter_storage: counter_storage, counter_repositories: counter_repositories, counter_build_artifacts: counter_build_artifacts, counter_lfs_objects: counter_lfs_objects }
%li
- %span.light Group Git LFS status:
+ %span.light= _('Group Git LFS status:')
%strong
= group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
@@ -67,7 +64,7 @@
.card
.card-header
%h3.card-title
- Projects
+ = _('Projects')
%span.badge.badge-pill
#{@group.projects.count}
%ul.content-list
@@ -85,7 +82,7 @@
- if @group.shared_projects.any?
.card
.card-header
- Projects shared with #{@group.name}
+ = _('Projects shared with %{group_name}') % { group_name: @group.name }
%span.badge.badge-pill
#{@group.shared_projects.count}
%ul.content-list
@@ -102,11 +99,11 @@
- if can?(current_user, :admin_group_member, @group)
.card
.card-header
- Add user(s) to the group:
+ = _('Add user(s) to the group:')
.card-body.form-holder
%p.light
- Read more about project permissions
- %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ - link_to_help = link_to(_("here"), help_page_path("user/permissions"), class: "vlink")
+ = _('Read more about project permissions <strong>%{link_to_help}</strong>').html_safe % { link_to_help: link_to_help }
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
@@ -114,16 +111,15 @@
.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
- = button_tag 'Add users to group', class: "btn btn-create"
+ = button_tag _('Add users to group'), class: "btn btn-create"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
.card
.card-header
- %strong= @group.name
- group members
+ = _("<strong>%{group_name}</strong> group members").html_safe % { group_name: @group.name }
%span.badge.badge-pill= @group.members.size
.float-right
- = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm"
+ = link_to icon('pencil-square-o', text: _('Manage access')), polymorphic_url([@group, :members]), class: "btn btn-sm"
%ul.content-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
.card-footer
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index a676eba2aee..9c246e19faa 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,8 +1,8 @@
.nav-block.activities
+ = render 'shared/event_filter'
.controls
= link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss
- = render 'shared/event_filter'
.content_list
= spinner
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 8b3974d97f8..d5a9cc646a6 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -30,33 +30,27 @@
.todos-filters
.row-content-block.second-block
- = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form d-sm-flex' do
- .filter-categories.flex-fill
- .filter-item.inline
- - if params[:group_id].present?
- = hidden_field_tag(:group_id, params[:group_id])
- = dropdown_tag(group_dropdown_label(params[:group_id], 'Group'), options: { toggle_class: 'js-group-search js-filter-submit', title: 'Filter by group', filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit',
- placeholder: 'Search groups', data: { data: todo_group_options, default_label: 'Group', display: 'static' } })
- .filter-item.inline
- - if params[:project_id].present?
- = hidden_field_tag(:project_id, params[:project_id])
- = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } })
- .filter-item.inline
- - if params[:author_id].present?
- = hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
- placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
- .filter-item.inline
- - if params[:type].present?
- = hidden_field_tag(:type, params[:type])
- = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options, default_label: 'Type' } })
- .filter-item.inline.actions-filter
- - if params[:action_id].present?
- = hidden_field_tag(:action_id, params[:action_id])
- = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options, default_label: 'Action' } })
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
+ .filter-item.inline
+ - if params[:project_id].present?
+ = hidden_field_tag(:project_id, params[:project_id])
+ = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } })
+ .filter-item.inline
+ - if params[:author_id].present?
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
+ placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
+ .filter-item.inline
+ - if params[:type].present?
+ = hidden_field_tag(:type, params[:type])
+ = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
+ data: { data: todo_types_options, default_label: 'Type' } })
+ .filter-item.inline.actions-filter
+ - if params[:action_id].present?
+ = hidden_field_tag(:action_id, params[:action_id])
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
+ data: { data: todo_actions_options, default_label: 'Action' } })
.filter-item.sort-filter
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' }
diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml
index a3b0709e261..eefc797cf03 100644
--- a/app/views/explore/_head.html.haml
+++ b/app/views/explore/_head.html.haml
@@ -1,6 +1,6 @@
.explore-title.text-center
%h2
- Explore GitLab
+ = _("Explore GitLab")
%p.lead
- Discover projects, groups and snippets. Share your projects with others
+ = _("Discover projects, groups and snippets. Share your projects with others")
%br
diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml
index ab4787c6d05..c337149a2f3 100644
--- a/app/views/explore/groups/_nav.html.haml
+++ b/app/views/explore/groups/_nav.html.haml
@@ -2,7 +2,7 @@
%ul.nav-links.nav.nav-tabs
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path do
- Explore Groups
+ = _("Explore Groups")
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 0643b9cfbc5..387c37b7a91 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -1,6 +1,6 @@
- @hide_top_links = true
-- page_title "Groups"
-- header_title "Groups", dashboard_groups_path
+- page_title _("Groups")
+- header_title _("Groups"), dashboard_groups_path
- if current_user
= render 'dashboard/groups_head'
@@ -10,14 +10,14 @@
- if cookies[:explore_groups_landing_dismissed] != 'true'
.explore-groups.landing.content-block.js-explore-groups-landing.hide
- %button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times')
+ %button.dismiss-button{ type: 'button', 'aria-label' => _('Dismiss') }= icon('times')
.svg-container
= custom_icon('icon_explore_groups_splash')
.inner-content
- %p Below you will find all the groups that are public.
- %p You can easily contribute to them by requesting to join these groups.
+ %p= _("Below you will find all the groups that are public.")
+ %p= _("You can easily contribute to them by requesting to join these groups.")
- if params[:filter].blank? && @groups.empty?
- .nothing-here-block No public groups
+ .nothing-here-block= _("No public groups")
- else
= render 'groups'
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 6abb56ba6d2..b694103ccaf 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -2,16 +2,16 @@
.dropdown
%button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
= icon('globe')
- %span.light Visibility:
+ %span.light= _("Visibility:")
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- else
- Any
+ = _('Any')
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right
%li
= link_to filter_projects_path(visibility_level: nil) do
- Any
+ = _('Any')
- Gitlab::VisibilityLevel.values.each do |level|
%li{ class: active_when(level.to_s == params[:visibility_level]) || 'light' }
= link_to filter_projects_path(visibility_level: level) do
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index 558cd26f1e0..bf65c19b720 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -2,13 +2,13 @@
%ul.nav-links.nav.nav-tabs
= nav_link(page: [trending_explore_projects_path, explore_root_path]) do
= link_to trending_explore_projects_path do
- Trending
+ = _('Trending')
= nav_link(page: starred_explore_projects_path) do
= link_to starred_explore_projects_path do
- Most stars
+ = _('Most stars')
= nav_link(page: explore_projects_path) do
= link_to explore_projects_path do
- All
+ = _('All')
.nav-controls
- unless current_user
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index f00802e0af7..452f390695c 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -1,6 +1,6 @@
- @hide_top_links = true
-- page_title "Projects"
-- header_title "Projects", dashboard_projects_path
+- page_title _("Projects")
+- header_title _("Projects"), dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index f00802e0af7..452f390695c 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -1,6 +1,6 @@
- @hide_top_links = true
-- page_title "Projects"
-- header_title "Projects", dashboard_projects_path
+- page_title _("Projects")
+- header_title _("Projects"), dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index f00802e0af7..452f390695c 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -1,6 +1,6 @@
- @hide_top_links = true
-- page_title "Projects"
-- header_title "Projects", dashboard_projects_path
+- page_title _("Projects")
+- header_title _("Projects"), dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 577c63503a8..82a497289f3 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,8 +1,8 @@
.nav-block.activities
+ = render 'shared/event_filter'
.controls
= link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss
- = render 'shared/event_filter'
.content_list
= spinner
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index 5e7be5cd37b..f0d1e837317 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -21,23 +21,13 @@
%th= _('Status')
%tbody
- @already_added_projects.each do |project|
- %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
+ %tr{ id: "project_#{project.id}", class: project_status_css_class(project.import_status) }
%td
= provider_project_link(provider, project.import_source)
%td
= link_to project.full_path, [project.namespace.becomes(Namespace), project]
%td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- = _('Done')
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- = _('Started')
- - elsif project.import_status == 'failed'
- = _('Failed')
- - else
- = project.human_import_status_name
+ = render 'import/project_status', project: project
- @repos.each do |repo|
%tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } }
@@ -61,6 +51,6 @@
= has_ci_cd_only_params? ? _('Connect') : _('Import')
= icon("spinner spin", class: "loading-icon")
-.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}",
- import_path: "#{url_for([:import, provider])}",
- ci_cd_only: "#{has_ci_cd_only_params?}" } }
+.js-importer-status{ data: { jobs_import_path: url_for([:jobs, :import, provider]),
+ import_path: url_for([:import, provider]),
+ ci_cd_only: has_ci_cd_only_params?.to_s } }
diff --git a/app/views/import/_project_status.html.haml b/app/views/import/_project_status.html.haml
new file mode 100644
index 00000000000..280bcbc1e63
--- /dev/null
+++ b/app/views/import/_project_status.html.haml
@@ -0,0 +1,11 @@
+- case project.import_status
+- when 'finished'
+ = icon('check')
+ = _('Done')
+- when 'started'
+ = icon("spinner spin")
+ = _('Started')
+- when 'failed'
+ = _('Failed')
+- else
+ = project.human_import_status_name
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
new file mode 100644
index 00000000000..763beb5958f
--- /dev/null
+++ b/app/views/import/manifest/_form.html.haml
@@ -0,0 +1,23 @@
+= form_tag upload_import_manifest_path, multipart: true do
+ .form-group
+ = label_tag :group_id, nil, class: 'label-light' do
+ = _('Group')
+ .input-group
+ .input-group-prepend.has-tooltip{ title: root_url }
+ .input-group-text
+ = root_url
+ = select_tag :group_id, namespaces_options(nil, display_path: true, groups_only: true), { class: 'select2 js-select-namespace' }
+ .form-text.text-muted
+ = _('Choose the top-level group for your repository imports.')
+
+ .form-group
+ = label_tag :manifest, class: 'label-light' do
+ = _('Manifest')
+ = file_field_tag :manifest, class: 'form-control-file', required: true
+ .form-text.text-muted
+ = _('Import multiple repositories by uploading a manifest file.')
+ = link_to icon('question-circle'), help_page_path('user/project/import/manifest')
+
+ .append-bottom-10
+ = submit_tag _('List available repositories'), class: 'btn btn-success'
+ = link_to _('Cancel'), new_project_path, class: 'btn btn-cancel'
diff --git a/app/views/import/manifest/new.html.haml b/app/views/import/manifest/new.html.haml
new file mode 100644
index 00000000000..056e4922b9e
--- /dev/null
+++ b/app/views/import/manifest/new.html.haml
@@ -0,0 +1,12 @@
+- page_title "Manifest file import"
+- header_title "Projects", root_path
+
+%h3.page-title
+ = _('Manifest file import')
+
+- if @errors.present?
+ .alert.alert-danger
+ - @errors.each do |error|
+ = error
+
+= render 'form'
diff --git a/app/views/import/manifest/status.html.haml b/app/views/import/manifest/status.html.haml
new file mode 100644
index 00000000000..5b2e1005398
--- /dev/null
+++ b/app/views/import/manifest/status.html.haml
@@ -0,0 +1,42 @@
+- page_title "Manifest import"
+- header_title "Projects", root_path
+- provider = 'manifest'
+
+%h3.page-title
+ = _('Manifest file import')
+
+%p
+ = button_tag class: "btn btn-import btn-success js-import-all" do
+ = import_all_githubish_repositories_button_label
+ = icon("spinner spin", class: "loading-icon")
+
+.table-responsive
+ %table.table.import-jobs
+ %thead
+ %tr
+ %th= _('Repository URL')
+ %th= _('To GitLab')
+ %th= _('Status')
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{ id: "project_#{project.id}", class: project_status_css_class(project.import_status) }
+ %td
+ = project.import_url
+ %td
+ = link_to_project project
+ %td.job-status
+ = render 'import/project_status', project: project
+
+ - @pending_repositories.each do |repository|
+ %tr{ id: "repo_#{repository[:id]}" }
+ %td
+ = repository[:url]
+ %td.import-target
+ = import_project_target(@group.full_path, repository[:path])
+ %td.import-actions.job-status
+ = button_tag class: "btn btn-import js-add-to-import" do
+ = _('Import')
+ = icon("spinner spin", class: "loading-icon")
+
+.js-importer-status{ data: { jobs_import_path: url_for([:jobs, :import, provider]),
+ import_path: url_for([:import, provider]) } }
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 1f701f2aa1b..6bf21570d41 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,10 +1,9 @@
%div{ class: container_class }
.nav-block.activity-filter-block.activities
+ = render 'shared/event_filter'
.controls
= link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn rss-btn has-tooltip' do
= icon('rss')
- = render 'shared/event_filter'
-
.content_list.project-activity{ :"data-href" => activity_project_path(@project) }
= spinner
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 8f535b9d789..3da6db08580 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -1,49 +1,62 @@
- active_tab = local_assigns.fetch(:active_tab, 'blank')
-- f = local_assigns.fetch(:f)
.project-import
.form-group.import-btn-container.clearfix
- = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
+ %h5
Import project from
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
- %div
- - if github_import_enabled?
+
+ - if github_import_enabled?
+ %div
= link_to new_import_github_path, class: 'btn js-import-github' do
= icon('github', text: 'GitHub')
- %div
- - if bitbucket_import_enabled?
+
+ - if bitbucket_import_enabled?
+ %div
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
- %div
- - if gitlab_import_enabled?
+
+ - if gitlab_import_enabled?
+ %div
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
- %div
- - if google_code_import_enabled?
+
+ - if google_code_import_enabled?
+ %div
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code')
- %div
- - if fogbugz_import_enabled?
+
+ - if fogbugz_import_enabled?
+ %div
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= icon('bug', text: 'Fogbugz')
- %div
- - if gitea_import_enabled?
+
+ - if gitea_import_enabled?
+ %div
= link_to new_import_gitea_path, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
- %div
- - if git_import_enabled?
+
+ - if git_import_enabled?
+ %div
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL')
+
+ - if manifest_import_enabled?
+ %div
+ = link_to new_import_manifest_path, class: 'btn import_manifest' do
+ = icon('file-text-o', text: 'Manifest file')
+
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
- %hr
+ = form_for @project, html: { class: 'new_project' } do |f|
+ %hr
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index 8a390cf8700..1a9ab288683 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,4 +1,4 @@
-.detail-page-description.content-block
+.detail-page-description
%h2.title
= markdown_field(@merge_request, :title)
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 5bb1bfb7059..6c363345e38 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -55,13 +55,12 @@
= render 'project_templates', f: f
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
- = form_for @project, html: { class: 'new_project' } do |f|
- - if import_sources_enabled?
- = render 'import_project_pane', f: f, active_tab: active_tab
- - else
- .nothing-here-block
- %h4 No import options available
- %p Contact an administrator to enable options for importing your project.
+ - if import_sources_enabled?
+ = render 'import_project_pane', active_tab: active_tab
+ - else
+ .nothing-here-block
+ %h4 No import options available
+ %p Contact an administrator to enable options for importing your project.
.save-project-loader.d-none
.center
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index ecb5b1c6ebc..7afb7b3a93b 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,4 @@
-.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller.flex-fill
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
diff --git a/bin/changelog b/bin/changelog
index d7b2a1a2de9..758c036161e 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -23,6 +23,8 @@ module ChangelogHelpers
Abort = Class.new(StandardError)
Done = Class.new(StandardError)
+ MAX_FILENAME_LENGTH = 140 # ecryptfs has a limit of 140 characters
+
def capture_stdout(cmd)
output = IO.popen(cmd, &:read)
fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
@@ -142,7 +144,9 @@ class ChangelogEntry
def initialize(options)
@options = options
+ end
+ def execute
assert_feature_branch!
assert_title!
assert_new_file!
@@ -221,10 +225,12 @@ class ChangelogEntry
end
def file_path
- File.join(
+ base_path = File.join(
unreleased_path,
- branch_name.gsub(/[^\w-]/, '-') << '.yml'
- )
+ branch_name.gsub(/[^\w-]/, '-'))
+
+ # Add padding for .yml extension
+ base_path[0..MAX_FILENAME_LENGTH - 5] + '.yml'
end
def unreleased_path
@@ -250,7 +256,7 @@ end
if $0 == __FILE__
begin
options = ChangelogOptionParser.parse(ARGV)
- ChangelogEntry.new(options)
+ ChangelogEntry.new(options).execute
rescue ChangelogHelpers::Abort => ex
$stderr.puts ex.message
exit 1
diff --git a/changelogs/unreleased/46930-fix-updated_at-if-created_at-is-set-note-api.yml b/changelogs/unreleased/46930-fix-updated_at-if-created_at-is-set-note-api.yml
new file mode 100644
index 00000000000..d95714a5267
--- /dev/null
+++ b/changelogs/unreleased/46930-fix-updated_at-if-created_at-is-set-note-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix updated_at if created_at is set for Note API
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/48237-toggle-file-comments.yml b/changelogs/unreleased/48237-toggle-file-comments.yml
new file mode 100644
index 00000000000..2e893aad0b2
--- /dev/null
+++ b/changelogs/unreleased/48237-toggle-file-comments.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes toggle discussion button not expanding collapsed discussions
+merge_request: 20452
+author:
+type: fixed
diff --git a/changelogs/unreleased/48789-remove-event-listeners-scroll.yml b/changelogs/unreleased/48789-remove-event-listeners-scroll.yml
new file mode 100644
index 00000000000..9cc3f7adc36
--- /dev/null
+++ b/changelogs/unreleased/48789-remove-event-listeners-scroll.yml
@@ -0,0 +1,6 @@
+---
+title: Improves performance on Merge Request diff tab by removing the scroll event
+ listeners being added to every file
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/48894-fix-rss-button-interaction.yml b/changelogs/unreleased/48894-fix-rss-button-interaction.yml
new file mode 100644
index 00000000000..546a4233d7e
--- /dev/null
+++ b/changelogs/unreleased/48894-fix-rss-button-interaction.yml
@@ -0,0 +1,5 @@
+---
+title: Fix RSS button interaction on Dashboard, Project and Group activities
+merge_request: 20549
+author:
+type: fixed
diff --git a/changelogs/unreleased/48934.yml b/changelogs/unreleased/48934.yml
new file mode 100644
index 00000000000..8e2e53ed198
--- /dev/null
+++ b/changelogs/unreleased/48934.yml
@@ -0,0 +1,5 @@
+---
+title: Improve danger confirmation modals by focusing input field
+merge_request:
+author: Jamie Schembri
+type: added
diff --git a/changelogs/unreleased/an-no-healthcheck-until-brooklyn.yml b/changelogs/unreleased/an-no-healthcheck-until-brooklyn.yml
new file mode 100644
index 00000000000..4942688d00f
--- /dev/null
+++ b/changelogs/unreleased/an-no-healthcheck-until-brooklyn.yml
@@ -0,0 +1,5 @@
+---
+title: Remove healthchecks from prometheus endpoint
+merge_request: 20565
+author:
+type: fixed
diff --git a/changelogs/unreleased/dz-manifest-import.yml b/changelogs/unreleased/dz-manifest-import.yml
new file mode 100644
index 00000000000..b0d29b0869f
--- /dev/null
+++ b/changelogs/unreleased/dz-manifest-import.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to import multiple repositories by uploading a manifest file
+merge_request: 20304
+author:
+type: added
diff --git a/changelogs/unreleased/fix-gb-add-missing-before-sha-predefined-variable.yml b/changelogs/unreleased/fix-gb-add-missing-before-sha-predefined-variable.yml
new file mode 100644
index 00000000000..7e9e8c33a71
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-add-missing-before-sha-predefined-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing predefined variable and fix docs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-fix-project-settings-build-time-validation.yml b/changelogs/unreleased/fix-gb-fix-project-settings-build-time-validation.yml
new file mode 100644
index 00000000000..adf582e34a2
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-project-settings-build-time-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Limit maximum project build timeout setting to 1 month
+merge_request: 20591
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-performance-problem-of-tags-query.yml b/changelogs/unreleased/fix-performance-problem-of-tags-query.yml
new file mode 100644
index 00000000000..4649775be9c
--- /dev/null
+++ b/changelogs/unreleased/fix-performance-problem-of-tags-query.yml
@@ -0,0 +1,5 @@
+---
+title: Fix performance problem of accessing tag list for projects api endpoints
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/gitaly-serverservice-info-timeout.yml b/changelogs/unreleased/gitaly-serverservice-info-timeout.yml
new file mode 100644
index 00000000000..7f2fe8b9c93
--- /dev/null
+++ b/changelogs/unreleased/gitaly-serverservice-info-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Use appropriate timeout on Gitaly server info checks, avoid error on timeout
+merge_request: 20552
+author:
+type: fixed
diff --git a/changelogs/unreleased/ide-pipeline-icon-open.yml b/changelogs/unreleased/ide-pipeline-icon-open.yml
new file mode 100644
index 00000000000..3a73ff2170f
--- /dev/null
+++ b/changelogs/unreleased/ide-pipeline-icon-open.yml
@@ -0,0 +1,5 @@
+---
+title: Clicking CI icon in Web IDE now opens up pipelines panel
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/ide-row-dropdown-design-update.yml b/changelogs/unreleased/ide-row-dropdown-design-update.yml
new file mode 100644
index 00000000000..e0fe64c944e
--- /dev/null
+++ b/changelogs/unreleased/ide-row-dropdown-design-update.yml
@@ -0,0 +1,5 @@
+---
+title: Updated design of new entry dropdown in Web IDE
+merge_request: 20526
+author:
+type: changed
diff --git a/changelogs/unreleased/issue_47709.yml b/changelogs/unreleased/issue_47709.yml
new file mode 100644
index 00000000000..c3ef55fd692
--- /dev/null
+++ b/changelogs/unreleased/issue_47709.yml
@@ -0,0 +1,5 @@
+---
+title: 'Allow to toggle notifications for issues due soon'
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-stderr-pipe-consumption.yml b/changelogs/unreleased/sh-fix-stderr-pipe-consumption.yml
new file mode 100644
index 00000000000..b7366cf2569
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-stderr-pipe-consumption.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid process deadlock in popen by consuming input pipes
+merge_request: 20600
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-optimize-wiki-empty-check.yml b/changelogs/unreleased/sh-optimize-wiki-empty-check.yml
new file mode 100644
index 00000000000..31ca7497b5a
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-wiki-empty-check.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize ProjectWiki#empty? check
+merge_request: 20573
+author:
+type: performance
diff --git a/changelogs/unreleased/winh-upgrade-grape-path-helpers.yml b/changelogs/unreleased/winh-upgrade-grape-path-helpers.yml
new file mode 100644
index 00000000000..62addff1d0f
--- /dev/null
+++ b/changelogs/unreleased/winh-upgrade-grape-path-helpers.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade grape-path-helpers to 1.0.6
+merge_request: 20601
+author:
+type: other
diff --git a/config/routes/import.rb b/config/routes/import.rb
index c378253bf15..efd0260ff60 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -45,4 +45,10 @@ namespace :import do
resource :gitlab_project, only: [:create, :new] do
post :create
end
+
+ resource :manifest, only: [:create, :new], controller: :manifest do
+ get :status
+ get :jobs
+ post :upload
+ end
end
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
new file mode 100644
index 00000000000..2424e650d07
--- /dev/null
+++ b/danger/changelog/Dangerfile
@@ -0,0 +1,66 @@
+# rubocop:disable Style/SignalException
+
+require 'yaml'
+
+NO_CHANGELOG_LABELS = %w[backstage QA test].freeze
+SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze
+MISSING_CHANGELOG_MESSAGE = <<~MSG.freeze
+**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html).**
+
+You can create one with:
+
+```
+bin/changelog -m %<mr_iid>s
+```
+
+If your merge request doesn't warrant a CHANGELOG entry,
+consider adding any of the %<labels>s labels.
+#{SEE_DOC}
+MSG
+
+def ee?
+ ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('../../CHANGELOG-EE.md')
+end
+
+def ee_changelog?(changelog_path)
+ changelog_path =~ /unreleased-ee/
+end
+
+def ce_port_changelog?(changelog_path)
+ ee? && !ee_changelog?(changelog_path)
+end
+
+def check_changelog(path)
+ yaml = YAML.safe_load(File.read(path))
+
+ fail "`title` should be set, in #{gitlab.html_link(path)}! #{SEE_DOC}" if yaml["title"].nil?
+ fail "`type` should be set, in #{gitlab.html_link(path)}! #{SEE_DOC}" if yaml["type"].nil?
+
+ if yaml["merge_request"].nil?
+ message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{gitlab.html_link(path)}. #{SEE_DOC}"
+ elsif yaml["merge_request"] != gitlab.mr_json["iid"] && !ce_port_changelog?(changelog_path)
+ fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
+ end
+rescue StandardError
+ # YAML could not be parsed, fail the build.
+ fail "#{gitlab.html_link(path)} isn't valid YAML! #{SEE_DOC}"
+end
+
+def presented_no_changelog_labels
+ NO_CHANGELOG_LABELS.map { |label| "~#{label}" }.join(', ')
+end
+
+changelog_needed = (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
+changelog_found = git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
+
+if git.modified_files.include?("CHANGELOG.md")
+ fail "CHANGELOG.md was edited. Please remove the additions and create an entry with `bin/changelog -m #{gitlab.mr_json["iid"]}` instead."
+end
+
+if changelog_needed
+ if changelog_found
+ check_changelog(changelog_found)
+ else
+ warn format(MISSING_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], labels: presented_no_changelog_labels)
+ end
+end
diff --git a/danger/changes_size/Dangerfile b/danger/changes_size/Dangerfile
new file mode 100644
index 00000000000..8251d0d5cbf
--- /dev/null
+++ b/danger/changes_size/Dangerfile
@@ -0,0 +1,17 @@
+# FIXME: git.info_for_file raises the following error
+# /usr/local/bundle/gems/git-1.4.0/lib/git/lib.rb:956:in `command': (Danger::DSLError)
+# [!] Invalid `Dangerfile` file:
+# [!] Invalid `Dangerfile` file: git '--git-dir=/builds/gitlab-org/gitlab-ce/.git' '--work-tree=/builds/gitlab-org/gitlab-ce' cat-file '-t' '' 2>&1:fatal: Not a valid object name
+# This seems to be the same as https://github.com/danger/danger/issues/535.
+
+# locale_files_updated = git.modified_files.select { |path| path.start_with?('locale') }
+# locale_files_updated.each do |locale_file_updated|
+# git_stats = git.info_for_file(locale_file_updated)
+# message "Git stats for #{locale_file_updated}: #{git_stats[:insertions]} insertions, #{git_stats[:deletions]} insertions"
+# end
+
+if git.lines_of_code > 2_000
+ warn "This merge request is definitely too big (more than #{git.lines_of_code} lines changed), please split it into multiple merge requests."
+elsif git.lines_of_code > 500
+ warn "This merge request is quite big (more than #{git.lines_of_code} lines changed), please consider splitting it into multiple merge requests."
+end
diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile
new file mode 100644
index 00000000000..6f48994945a
--- /dev/null
+++ b/danger/database/Dangerfile
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+# All the files/directories that should be reviewed by the DB team.
+DB_FILES = [
+ 'db/',
+ 'app/models/project_authorization.rb',
+ 'app/services/users/refresh_authorized_projects_service.rb',
+ 'lib/gitlab/background_migration.rb',
+ 'lib/gitlab/background_migration/',
+ 'lib/gitlab/database.rb',
+ 'lib/gitlab/database/',
+ 'lib/gitlab/github_import.rb',
+ 'lib/gitlab/github_import/',
+ 'lib/gitlab/sql/',
+ 'rubocop/cop/migration',
+ 'ee/db/',
+ 'ee/lib/gitlab/database/'
+].freeze
+
+SCHEMA_NOT_UPDATED_MESSAGE = <<~MSG
+**New %<migrations>s added but %<schema>s wasn't updated.**
+
+Usually, when adding new %<migrations>s, %<schema>s should be
+updated too (unless the migration isn't changing the DB schema
+and isn't the most recent one).
+MSG
+
+def database_paths_requiring_review(files)
+ to_review = []
+
+ files.each do |file|
+ review = DB_FILES.any? do |pattern|
+ file.start_with?(pattern)
+ end
+
+ to_review << file if review
+ end
+
+ to_review
+end
+
+all_files = git.added_files + git.modified_files
+
+non_geo_db_schema_updated = !git.modified_files.grep(%r{\Adb/schema\.rb/}).empty?
+geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb/}).empty?
+
+non_geo_migration_created = !git.added_files.grep(%r{\A(db/(post_)?migrate)/}).empty?
+geo_migration_created = !git.added_files.grep(%r{\Aee/db/geo/(post_)?migrate/}).empty?
+
+if non_geo_migration_created && !non_geo_db_schema_updated
+ warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'migrations', schema: gitlab.html_link("db/schema.rb"))
+end
+
+if geo_migration_created && !geo_db_schema_updated
+ warn format(SCHEMA_NOT_UPDATED_MESSAGE, migrations: 'Geo migrations', schema: gitlab.html_link("ee/db/geo/schema.rb"))
+end
+
+db_paths_to_review = database_paths_requiring_review(all_files)
+
+unless db_paths_to_review.empty?
+ message 'This merge request adds or changes files that require a ' \
+ 'review from the Database team.'
+
+ markdown(<<~MARKDOWN.strip)
+## Database Review
+
+The following files require a review from the Database team:
+
+* #{db_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
+
+To make sure these changes are reviewed, take the following steps:
+
+1. Edit your merge request, and add `gl-database` to the list of Group
+ approvers.
+1. Mention `@gl-database` in a separate comment, and explain what needs to be
+ reviewed by the team. Please don't mention the team until your changes are
+ ready for review.
+ MARKDOWN
+
+ unless gitlab.mr_labels.include?('database')
+ warn 'This merge request is missing the ~database label.'
+ end
+end
diff --git a/danger/gemfile/Dangerfile b/danger/gemfile/Dangerfile
new file mode 100644
index 00000000000..8ef4a464fe4
--- /dev/null
+++ b/danger/gemfile/Dangerfile
@@ -0,0 +1,24 @@
+GEMFILE_LOCK_NOT_UPDATED_MESSAGE = <<~MSG.freeze
+**%<gemfile>s was updated but %<gemfile_lock>s wasn't updated.**
+
+Usually, when %<gemfile>s is updated, you should run
+```
+bundle install && \
+ BUNDLE_GEMFILE=Gemfile.rails5 bundle install
+```
+
+or
+
+```
+bundle update <the-added-or-updated-gem>
+```
+
+and commit the %<gemfile_lock>s changes.
+MSG
+
+gemfile_modified = git.modified_files.include?("Gemfile")
+gemfile_lock_modified = git.modified_files.include?("Gemfile.lock")
+
+if gemfile_modified && !gemfile_lock_modified
+ warn format(GEMFILE_LOCK_NOT_UPDATED_MESSAGE, gemfile: gitlab.html_link("Gemfile"), gemfile_lock: gitlab.html_link("Gemfile.lock"))
+end
diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile
new file mode 100644
index 00000000000..3cfaa04e01b
--- /dev/null
+++ b/danger/metadata/Dangerfile
@@ -0,0 +1,25 @@
+# rubocop:disable Style/SignalException
+
+if gitlab.mr_body.size < 5
+ fail "Please provide a proper merge request description."
+end
+
+if gitlab.mr_labels.empty?
+ fail "Please add labels to this merge request."
+end
+
+unless gitlab.mr_json["assignee"]
+ warn "This merge request does not have any assignee yet. Setting an assignee clarifies who needs to take action on the merge request at any given time."
+end
+
+has_milestone = !gitlab.mr_json["milestone"].nil?
+
+unless has_milestone
+ warn "This merge request does not refer to an existing milestone.", sticky: false
+end
+
+has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('Pick into') }
+
+if gitlab.branch_for_base != "master" && !has_pick_into_stable_label
+ warn "Most of the time, all merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
+end
diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile
new file mode 100644
index 00000000000..934ea0beadb
--- /dev/null
+++ b/danger/specs/Dangerfile
@@ -0,0 +1,12 @@
+NO_NEW_SPEC_MESSAGE = <<~MSG.freeze
+You've made some app changes, but didn't add any tests.
+That's OK as long as you're refactoring existing code,
+but please consider adding the ~backstage label in that case.
+MSG
+
+has_app_changes = !git.modified_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty?
+has_spec_changes = !git.modified_files.grep(/spec/).empty?
+
+if has_app_changes && !has_spec_changes
+ warn NO_NEW_SPEC_MESSAGE, sticky: false
+end
diff --git a/db/fixtures/development/19_environments.rb b/db/fixtures/development/19_environments.rb
index 65089f6ba4e..3e227928a29 100644
--- a/db/fixtures/development/19_environments.rb
+++ b/db/fixtures/development/19_environments.rb
@@ -30,7 +30,7 @@ class Gitlab::Seeder::Environments
def create_merge_request_review_deployments!
@project
.merge_requests
- .select { |mr| mr.source_branch.match(/[^a-zA-Z0-9]+/) }
+ .select { |mr| mr.source_branch.match?(/[a-zA-Z0-9]+/) }
.sample(4)
.each do |merge_request|
next unless merge_request.diff_head_sha
diff --git a/db/migrate/20180608091413_add_group_to_todos.rb b/db/migrate/20180608091413_add_group_to_todos.rb
deleted file mode 100644
index af3ee48b29d..00000000000
--- a/db/migrate/20180608091413_add_group_to_todos.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-class AddGroupToTodos < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- disable_ddl_transaction!
-
- def up
- add_column :todos, :group_id, :integer
- add_concurrent_foreign_key :todos, :namespaces, column: :group_id, on_delete: :cascade
- add_concurrent_index :todos, :group_id
-
- change_column_null :todos, :project_id, true
- end
-
- def down
- return unless group_id_exists?
-
- remove_foreign_key :todos, column: :group_id
- remove_index :todos, :group_id if index_exists?(:todos, :group_id)
- remove_column :todos, :group_id
-
- execute "DELETE FROM todos WHERE project_id IS NULL"
- change_column_null :todos, :project_id, false
- end
-
- private
-
- def group_id_exists?
- column_exists?(:todos, :group_id)
- end
-end
diff --git a/db/schema.rb b/db/schema.rb
index 75a1960e2f8..d2aa31fae30 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1950,7 +1950,7 @@ ActiveRecord::Schema.define(version: 20180704204006) do
create_table "todos", force: :cascade do |t|
t.integer "user_id", null: false
- t.integer "project_id"
+ t.integer "project_id", null: false
t.integer "target_id"
t.string "target_type", null: false
t.integer "author_id", null: false
@@ -1960,12 +1960,10 @@ ActiveRecord::Schema.define(version: 20180704204006) do
t.datetime "updated_at"
t.integer "note_id"
t.string "commit_id"
- t.integer "group_id"
end
add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree
add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree
- add_index "todos", ["group_id"], name: "index_todos_on_group_id", using: :btree
add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
@@ -2339,7 +2337,6 @@ ActiveRecord::Schema.define(version: 20180704204006) do
add_foreign_key "term_agreements", "users", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
- add_foreign_key "todos", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "todos", "notes", name: "fk_91d1f47b13", on_delete: :cascade
add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade
add_foreign_key "todos", "users", column: "author_id", name: "fk_ccf0373936", on_delete: :cascade
diff --git a/doc/api/README.md b/doc/api/README.md
index 6267618d3bc..4566319ad45 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -388,7 +388,7 @@ For example, `/` is represented by `%2F`:
GET /api/v4/projects/diaspora%2Fdiaspora
```
-## Branches & tags name encoding
+## Branches and tags name encoding
If your branch or tag contains a `/`, make sure the branch/tag name is
URL-encoded.
@@ -399,6 +399,36 @@ For example, `/` is represented by `%2F`:
GET /api/v4/projects/1/branches/my%2Fbranch/commits
```
+## Encoding API parameters of `array` and `hash` types
+
+When making an API call with parameters of type `array` and/or `hash`, the parameters may be
+specified as shown below.
+
+### `array`
+
+`import_sources` is a parameter of type `array`:
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" \
+-d "import_sources[]=github" \
+-d "import_sources[]=bitbucket" \
+"https://gitlab.example.com/api/v4/some_endpoint
+```
+
+### `hash`
+
+`override_params` is a parameter of type `hash`:
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" \
+--form "namespace=email" \
+--form "path=impapi" \
+--form "file=@/path/to/somefile.txt"
+--form "override_params[visibility]=private" \
+--form "override_params[some_other_param]=some_value" \
+https://gitlab.example.com/api/v4/projects/import
+```
+
## `id` vs `iid`
When you work with the API, you may notice two similar fields in API entities:
diff --git a/doc/api/notes.md b/doc/api/notes.md
index d29c5b94915..c271d46688f 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -218,6 +218,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `snippet_id` (required) - The ID of a snippet
- `body` (required) - The content of a note
+- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
@@ -340,6 +341,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `merge_request_iid` (required) - The IID of a merge request
- `body` (required) - The content of a note
+- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
### Modify existing merge request note
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index 085437c801a..83e405141f1 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -28,8 +28,11 @@ POST /projects/:id/export
| `upload[url]` | string | yes | The URL to upload the project |
| `upload[http_method]` | string | no | The HTTP method to upload the exported project. Only `PUT` and `POST` methods allowed. Default is `PUT` |
+
```console
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export --data "description=FooBar&upload[http_method]=PUT&upload[url]=https://example-bucket.s3.eu-west-3.amazonaws.com/backup?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMBJHN2O62W8IELQ%2F20180312%2Feu-west-3%2Fs3%2Faws4_request&X-Amz-Date=20180312T110328Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=8413facb20ff33a49a147a0b4abcff4c8487cc33ee1f7e450c46e8f695569dbd"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export \
+ --data "upload[http_method]=PUT" \
+ --data-urlencode "upload[url]=https://example-bucket.s3.eu-west-3.amazonaws.com/backup?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMBJHN2O62W8IELQ%2F20180312%2Feu-west-3%2Fs3%2Faws4_request&X-Amz-Date=20180312T110328Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=8413facb20ff33a49a147a0b4abcff4c8487cc33ee1f7e450c46e8f695569dbd"
```
```json
@@ -125,6 +128,29 @@ by `@`. For example:
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "path=api-project" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/projects/import
```
+cURL doesn't support posting a file from a remote server. Importing a project from a remote server can be accomplished through something like the following:
+
+```python
+import requests
+import urllib
+import json
+import sys
+
+s3_file = urllib.urlopen(presigned_url)
+
+url = 'https://gitlab.example.com/api/v4/projects/import'
+files = {'file': s3_file}
+data = {
+ "path": "example-project",
+ "namespace": "example-group"
+}
+headers = {
+ 'Private-Token': "9koXpg98eAheJpvBs5tK"
+}
+
+requests.post(url, headers=headers, data=data, files=files)
+```
+
```json
{
"id": 1,
diff --git a/doc/api/settings.md b/doc/api/settings.md
index e6b207d8746..b6f2101fc7b 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -105,7 +105,7 @@ PUT /application/settings
| `housekeeping_gc_period` | integer | no | Number of Git pushes after which 'git gc' is run. |
| `housekeeping_incremental_repack_period` | integer | no | Number of Git pushes after which an incremental 'git repack' is run. |
| `html_emails_enabled` | boolean | no | Enable HTML emails |
-| `import_sources` | Array of strings | no | Sources to allow project import from, possible values: "github bitbucket gitlab google_code fogbugz git gitlab_project |
+| `import_sources` | Array of strings | no | Sources to allow project import from, possible values: "github bitbucket gitlab google_code fogbugz git gitlab_project manifest |
| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
| `max_artifacts_size` | integer | no | Maximum artifacts size in MB |
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 0843e4eedc6..27e623007cc 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -18,7 +18,6 @@ Parameters:
| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, `approval_required`, `unmergeable` or `directly_addressed`. |
| `author_id` | integer | no | The ID of an author |
| `project_id` | integer | no | The ID of a project |
-| `group_id` | integer | no | The ID of a group |
| `state` | string | no | The state of the todo. Can be either `pending` or `done` |
| `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` |
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 9f6476edc34..84bd64d50cd 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -47,6 +47,7 @@ future GitLab releases.**
| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
+| **CI_COMMIT_BEFORE_SHA** | 11.2 | all | The previous latest commit present on a branch before a push request. |
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
@@ -118,6 +119,7 @@ future GitLab releases.**
| `CI_BUILD_ID` | `CI_JOB_ID` |
| `CI_BUILD_REF` | `CI_COMMIT_SHA` |
| `CI_BUILD_TAG` | `CI_COMMIT_TAG` |
+| `CI_BUILD_BEFORE_SHA` | `CI_COMMIT_BEFORE_SHA` |
| `CI_BUILD_REF_NAME` | `CI_COMMIT_REF_NAME` |
| `CI_BUILD_REF_SLUG` | `CI_COMMIT_REF_SLUG` |
| `CI_BUILD_NAME` | `CI_JOB_NAME` |
diff --git a/doc/development/fe_guide/img/vue_arch.png b/doc/development/fe_guide/img/vue_arch.png
deleted file mode 100644
index a67706c7c1e..00000000000
--- a/doc/development/fe_guide/img/vue_arch.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 219b98ac696..f6cbd11042c 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -2,27 +2,24 @@
To get started with Vue, read through [their documentation][vue-docs].
-## Vue architecture
+## Examples
-All new features built with Vue.js must follow a [Flux architecture][flux].
-The main goal we are trying to achieve is to have only one data flow and only one data entry.
-In order to achieve this goal, you can either use [vuex](#vuex) or use the [store pattern][state-management], explained below:
+What is described in the following sections can be found in these examples:
-Each Vue bundle needs a Store - where we keep all the data -, a Service - that we use to communicate with the server - and a main Vue component.
+- web ide: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/ide/stores
+- security products: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports
+- registry: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/registry/stores
-Think of the Main Vue Component as the entry point of your application. This is the only smart
-component that should exist in each Vue feature.
-This component is responsible for:
-1. Calling the Service to get data from the server
-1. Calling the Store to store the data received
-1. Mounting all the other components
+## Vue architecture
-![Vue Architecture](img/vue_arch.png)
+All new features built with Vue.js must follow a [Flux architecture][flux].
+The main goal we are trying to achieve is to have only one data flow and only one data entry.
+In order to achieve this goal we use [vuex](#vuex).
You can also read about this architecture in vue docs about [state management][state-management]
and about [one way data flow][one-way-data-flow].
-### Components, Stores and Services
+### Components and Store
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
@@ -33,10 +30,8 @@ new_feature
├── components
│ └── component.vue
│ └── ...
-├── stores
+├── store
│ └── new_feature_store.js
-├── services # only when not using vuex
-│ └── new_feature_service.js
├── index.js
```
_For consistency purposes, we recommend you to follow the same structure._
@@ -125,217 +120,6 @@ You can read more about components in Vue.js site, [Component System][component-
#### Vuex
Check this [page](vuex.md) for more details.
-#### Flux like state management
-The Store is a class that allows us to manage the state in a single
-source of truth. It is not aware of the service or the components.
-
-The concept we are trying to follow is better explained by Vue documentation
-itself, please read this guide: [State Management][state-management]
-
-### A folder for the Service
-
-**If you are using Vuex you won't need this step**
-
-The Service is a class used only to communicate with the server.
-It does not store or manipulate any data. It is not aware of the store or the components.
-We use [axios][axios] to communicate with the server.
-Refer to [axios](axios.md) for more details.
-
-Axios instance should only be imported in the service file.
-
-```javascript
-import axios from '~/lib/utils/axios_utils';
-```
-
-### End Result
-
-The following example shows an application:
-
-```javascript
-// store.js
-export default class Store {
-
- /**
- * This is where we will iniatialize the state of our data.
- * Usually in a small SPA you don't need any options when starting the store.
- * In that case you do need guarantee it's an Object and it's documented.
- *
- * @param {Object} options
- */
- constructor(options) {
- this.options = options;
-
- // Create a state object to handle all our data in the same place
- this.todos = [];
- }
-
- setTodos(todos = []) {
- this.todos = todos;
- }
-
- addTodo(todo) {
- this.todos.push(todo);
- }
-
- removeTodo(todoID) {
- const state = this.todos;
-
- const newState = state.filter((element) => {element.id !== todoID});
-
- this.todos = newState;
- }
-}
-
-// service.js
-import axios from '~/lib/utils/axios_utils'
-
-export default class Service {
- constructor(options) {
- this.todos = axios.create({
- baseURL: endpoint.todosEndpoint
- });
-
- }
-
- getTodos() {
- return this.todos.get();
- }
-
- addTodo(todo) {
- return this.todos.put(todo);
- }
-}
-// todo_component.vue
-<script>
-export default {
- props: {
- data: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-<template>
- <div>
- <h1>
- Title: {{data.title}}
- </h1>
- <p>
- {{data.text}}
- </p>
- </div>
-</template>
-
-// todos_main_component.vue
-<script>
-import Store from 'store';
-import Service from 'service';
-import TodoComponent from 'todoComponent';
-export default {
- components: {
- todo: TodoComponent,
- },
- /**
- * Although most data belongs in the store, each component it's own state.
- * We want to show a loading spinner while we are fetching the todos, this state belong
- * in the component.
- *
- * We need to access the store methods through all methods of our component.
- * We need to access the state of our store.
- */
- data() {
- const store = new Store();
-
- return {
- isLoading: false,
- store: store,
- todos: store.todos,
- };
- },
-
- created() {
- this.service = new Service('/todos');
-
- this.getTodos();
- },
-
- methods: {
- getTodos() {
- this.isLoading = true;
-
- this.service
- .getTodos()
- .then(response => {
- this.store.setTodos(response);
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- // Show an error
- });
- },
-
- addTodo(event) {
- this.service
- .addTodo({
- title: 'New entry',
- text: `You clicked on ${event.target.tagName}`,
- })
- .then(response => {
- this.store.addTodo(response);
- })
- .catch(() => {
- // Show an error
- });
- },
- },
-};
-</script>
-<template>
- <div class="container">
- <div v-if="isLoading">
- <i
- class="fa fa-spin fa-spinner"
- aria-hidden="true" />
- </div>
-
- <div
- v-if="!isLoading"
- class="js-todo-list">
- <template v-for='todo in todos'>
- <todo :data="todo" />
- </template>
-
- <button
- @click="addTodo"
- class="js-add-todo">
- Add Todo
- </button>
- </div>
- <div>
-</template>
-
-// index.js
-import todoComponent from 'todos_main_component.vue';
-
-new Vue({
- el: '.js-todo-app',
- components: {
- todoComponent,
- },
- render: createElement => createElement('todo-component' {
- props: {
- someProp: [],
- }
- }),
-});
-
-```
-
-The [issue boards service][issue-boards-service]
-is a good example of this pattern.
-
## Style guide
Please refer to the Vue section of our [style guide](style_guide_js.md#vue-js)
@@ -446,6 +230,5 @@ need to test the rendered output. [Vue][vue-test] guide's to unit test show us e
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
-[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[flux]: https://facebook.github.io/flux
[axios]: https://github.com/axios/axios
diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md
index 2d8fdf0d1da..ec8467da14f 100644
--- a/doc/user/project/clusters/eks_and_gitlab/index.md
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -62,7 +62,7 @@ Click on `Add Kubernetes cluster`, the cluster is now connected to GitLab. At th
If you would like to utilize your own CI/CD scripts to deploy to the cluster, you can stop here.
-## Disable Role Based-Access Control (RBAC)
+## Disable Role-Based Access Control (RBAC)
Presently, Auto DevOps and one-click app installs do not support [Kubernetes role-based access control](https://kubernetes.io/docs/reference/access-authn-authz/rbac/). Support is [being worked on](https://gitlab.com/groups/gitlab-org/-/epics/136), but in the interim RBAC must be disabled to utilize for these features.
diff --git a/doc/user/project/import/img/manifest_status.png b/doc/user/project/import/img/manifest_status.png
new file mode 100644
index 00000000000..b706116a2ac
--- /dev/null
+++ b/doc/user/project/import/img/manifest_status.png
Binary files differ
diff --git a/doc/user/project/import/img/manifest_upload.png b/doc/user/project/import/img/manifest_upload.png
new file mode 100644
index 00000000000..d6bf4b157dd
--- /dev/null
+++ b/doc/user/project/import/img/manifest_upload.png
Binary files differ
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 72cc58546b7..b55435e5b4f 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -11,6 +11,7 @@
1. [From SVN](svn.md)
1. [From TFS](tfs.md)
1. [From repo by URL](repo_by_url.md)
+1. [By uploading a manifest file](manifest.md)
In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the
diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md
new file mode 100644
index 00000000000..812ecf05faf
--- /dev/null
+++ b/doc/user/project/import/manifest.md
@@ -0,0 +1,48 @@
+# Import multiple repositories by uploading a manifest file
+
+GitLab allows you to import all the required git repositories
+based a manifest file like the one used by the Android repository.
+
+
+>**Note:**
+This feature requires [subgroups](../../group/subgroups/index.md) to be supported by your database.
+
+You can do it by following next steps:
+
+1. From your GitLab dashboard click **New project**
+1. Switch to the **Import project** tab
+1. Click on the **Manifest file** button
+1. Provide GitLab with a manifest xml file
+1. Select a group you want to import to (you need to create a group first if you don't have one)
+1. Click **List available repositories**
+1. You will be redirected to the import status page with projects list based on manifest file
+1. Check the list and click 'Import all repositories' to start import.
+
+![Manifest upload](img/manifest_upload.png)
+
+![Manifest status](img/manifest_status.png)
+
+### Manifest format
+
+A manifest must be an XML file. There must be one `remote` tag with `review` attribute
+that contains a URL to a git server. Each `project` tag must have `name` and `path` attribute.
+GitLab will build URL to the repository by combining URL from `remote` tag with a project name.
+A path attribute will be used to represent project path in GitLab system.
+
+Below is a valid example of manifest file.
+
+```xml
+<manifest>
+ <remote review="https://android-review.googlesource.com/" />
+
+ <project path="build/make" name="platform/build" />
+ <project path="build/blueprint" name="platform/build/blueprint" />
+</manifest>
+```
+
+As result next projects will be created:
+
+| GitLab | Import URL |
+|---|---|
+| https://gitlab/YOUR_GROUP/build/make | https://android-review.googlesource.com/platform/build |
+| https://gitlab/YOUR_GROUP/build/blueprint | https://android-review.googlesource.com/platform/build/blueprint |
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index e97b5d05529..860edb8e6f7 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -364,12 +364,12 @@ When dragging issues between lists, different behavior occurs depending on the s
Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table:
-| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Project Issue Boards | Configurable Group Issue Boards | Assignee Lists
+| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Issue Boards | Assignee Lists
| --- | --- | --- | --- | --- | --- |
-| Core | 1 | 1 | No | No | No |
-| Starter | Multiple | 1 | Yes | No | No |
-| Premium | Multiple | Multiple | Yes | Yes | Yes |
-| Ultimate | Multiple | Multiple | Yes | Yes | Yes |
+| Core | 1 | 1 | No | No |
+| Starter | Multiple | 1 | Yes | No |
+| Premium | Multiple | Multiple | Yes | Yes |
+| Ultimate | Multiple | Multiple | Yes | Yes |
## Tips
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index dda82352c67..760cd87d4cc 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -109,7 +109,6 @@ There are four kinds of filters you can use on your Todos dashboard.
| Filter | Description |
| ------- | ----------- |
| Project | Filter by project |
-| Group | Filter by group |
| Author | Filter by the author that triggered the Todo |
| Type | Filter by issue or merge request |
| Action | Filter by the action that triggered the Todo |
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 40df1e79bc7..b256c33c631 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -135,10 +135,13 @@ module API
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
def self.preload_relation(projects_relation, options = {})
+ # Preloading tags, should be done with using only `:tags`,
+ # as `:tags` are defined as: `has_many :tags, through: :taggings`
+ # N+1 is solved then by using `subject.tags.map(&:name)`
+ # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
projects_relation.preload(:project_feature, :route)
- .preload(:import_state)
- .preload(namespace: [:route, :owner],
- tags: :taggings)
+ .preload(:import_state, :tags)
+ .preload(namespace: [:route, :owner])
end
end
@@ -212,11 +215,15 @@ module API
expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics
def self.preload_relation(projects_relation, options = {})
+ # Preloading tags, should be done with using only `:tags`,
+ # as `:tags` are defined as: `has_many :tags, through: :taggings`
+ # N+1 is solved then by using `subject.tags.map(&:name)`
+ # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555
super(projects_relation).preload(:group)
.preload(project_group_links: :group,
fork_network: :root_project,
forked_project_link: :forked_from_project,
- forked_from_project: [:route, :forks, namespace: :route, tags: :taggings])
+ forked_from_project: [:route, :forks, :tags, namespace: :route])
end
def self.forks_counting_projects(projects_relation)
@@ -775,33 +782,28 @@ module API
class Todo < Grape::Entity
expose :id
- expose :project, using: Entities::ProjectIdentity, if: -> (todo, _) { todo.project_id }
- expose :group, using: 'API::Entities::NamespaceBasic', if: -> (todo, _) { todo.group_id }
+ expose :project, using: Entities::BasicProjectDetails
expose :author, using: Entities::UserBasic
expose :action_name
expose :target_type
expose :target do |todo, options|
- todo_target_class(todo.target_type).represent(todo.target, options)
+ Entities.const_get(todo.target_type).represent(todo.target, options)
end
expose :target_url do |todo, options|
target_type = todo.target_type.underscore
- target_url = "#{todo.parent.class.to_s.underscore}_#{target_type}_url"
+ target_url = "namespace_project_#{target_type}_url"
target_anchor = "note_#{todo.note_id}" if todo.note_id?
Gitlab::Routing
.url_helpers
- .public_send(target_url, todo.parent, todo.target, anchor: target_anchor) # rubocop:disable GitlabSecurity/PublicSend
+ .public_send(target_url, todo.project.namespace, todo.project, todo.target, anchor: target_anchor) # rubocop:disable GitlabSecurity/PublicSend
end
expose :body
expose :state
expose :created_at
-
- def todo_target_class(target_type)
- ::API::Entities.const_get(target_type)
- end
end
class NamespaceBasic < Grape::Entity
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index b4bfb677d72..e2984b08eca 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -97,6 +97,8 @@ module API
current_user.admin? || parent.owned_by?(current_user)
end
+ opts[:updated_at] = opts[:created_at] if opts[:created_at]
+
project = parent if parent.is_a?(Project)
::Notes::CreateService.new(project, current_user, opts).execute
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 02ef89f997f..1ca7d23203b 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -25,7 +25,7 @@ module API
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
- optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
+ optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project manifest],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
diff --git a/lib/gitaly/server.rb b/lib/gitaly/server.rb
index 2760211fee8..f95e423ef22 100644
--- a/lib/gitaly/server.rb
+++ b/lib/gitaly/server.rb
@@ -50,7 +50,7 @@ module Gitaly
@info ||=
begin
Gitlab::GitalyClient::ServerService.new(@storage).info
- rescue GRPC::Unavailable, GRPC::GRPC::DeadlineExceeded
+ rescue GRPC::Unavailable, GRPC::DeadlineExceeded
# This will show the server as being out of date
Gitaly::ServerInfoResponse.new(git_version: '', server_version: '', storage_statuses: [])
end
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index f9f24ecc48d..7426688fc55 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -21,6 +21,10 @@ module Gitlab
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
stdout.set_encoding(Encoding::ASCII_8BIT)
+ # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
+ # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
+ err_reader = Thread.new { stderr.read }
+
yield(stdin) if block_given?
stdin.close
@@ -32,7 +36,7 @@ module Gitlab
cmd_output << stdout.read
end
- cmd_output << stderr.read
+ cmd_output << err_reader.value
cmd_status = wait_thr.value.exitstatus
end
@@ -55,16 +59,20 @@ module Gitlab
rerr, werr = IO.pipe
pid = Process.spawn(vars, *cmd, out: wout, err: werr, chdir: path, pgroup: true)
+ # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
+ # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
+ out_reader = Thread.new { rout.read }
+ err_reader = Thread.new { rerr.read }
begin
- status = process_wait_with_timeout(pid, timeout)
-
# close write ends so we could read them
wout.close
werr.close
- cmd_output = rout.readlines.join
- cmd_output << rerr.readlines.join # Copying the behaviour of `popen` which merges stderr into output
+ status = process_wait_with_timeout(pid, timeout)
+
+ cmd_output = out_reader.value
+ cmd_output << err_reader.value # Copying the behaviour of `popen` which merges stderr into output
[cmd_output, status.exitstatus]
rescue Timeout::Error => e
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index a1a050647b9..fc4711751b1 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -86,9 +86,6 @@ module Gitlab
# Relative path of repo
attr_reader :relative_path
- # Rugged repo object
- attr_reader :rugged
-
attr_reader :gitlab_projects, :storage, :gl_repository, :relative_path
# This initializer method is only used on the client side (gitlab-ce).
@@ -112,8 +109,9 @@ module Gitlab
[storage, relative_path] == [other.storage, other.relative_path]
end
+ # This method will be removed when Gitaly reaches v1.1.
def path
- @path ||= File.join(
+ File.join(
Gitlab.config.repositories.storages[@storage].legacy_disk_path, @relative_path
)
end
@@ -127,8 +125,9 @@ module Gitlab
raise Gitlab::Git::CommandError.new(e.message)
end
+ # This method will be removed when Gitaly reaches v1.1.
def rugged
- @rugged ||= circuit_breaker.perform do
+ circuit_breaker.perform do
Rugged::Repository.new(path, alternates: alternate_object_directories)
end
rescue Rugged::RepositoryError, Rugged::OSError
@@ -168,24 +167,9 @@ module Gitlab
# Directly find a branch with a simple name (e.g. master)
#
- # force_reload causes a new Rugged repository to be instantiated
- #
- # This is to work around a bug in libgit2 that causes in-memory refs to
- # be stale/invalid when packed-refs is changed.
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
- def find_branch(name, force_reload = false)
- gitaly_migrate(:find_branch) do |is_enabled|
- if is_enabled
- gitaly_ref_client.find_branch(name)
- else
- reload_rugged if force_reload
-
- rugged_ref = rugged.branches[name]
- if rugged_ref
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
- end
- end
+ def find_branch(name)
+ wrapped_gitaly_errors do
+ gitaly_ref_client.find_branch(name)
end
end
@@ -197,20 +181,8 @@ module Gitlab
# Returns the number of valid branches
def branch_count
- gitaly_migrate(:branch_names) do |is_enabled|
- if is_enabled
- gitaly_ref_client.count_branch_names
- else
- rugged.branches.each(:local).count do |ref|
- begin
- ref.name && ref.target # ensures the branch is valid
-
- true
- rescue Rugged::ReferenceError
- false
- end
- end
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.count_branch_names
end
end
@@ -233,12 +205,8 @@ module Gitlab
# Returns the number of valid tags
def tag_count
- gitaly_migrate(:tag_names) do |is_enabled|
- if is_enabled
- gitaly_ref_client.count_tag_names
- else
- rugged.tags.count
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.count_tag_names
end
end
@@ -261,13 +229,8 @@ module Gitlab
#
# Ref names must start with `refs/`.
def ref_exists?(ref_name)
- gitaly_migrate(:ref_exists,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_ref_exists?(ref_name)
- else
- rugged_ref_exists?(ref_name)
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_exists?(ref_name)
end
end
@@ -275,12 +238,8 @@ module Gitlab
#
# name - The name of the tag as a String.
def tag_exists?(name)
- gitaly_migrate(:ref_exists_tags, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_ref_exists?("refs/tags/#{name}")
- else
- rugged_tag_exists?(name)
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_exists?("refs/tags/#{name}")
end
end
@@ -288,12 +247,8 @@ module Gitlab
#
# name - The name of the branch as a String.
def branch_exists?(name)
- gitaly_migrate(:ref_exists_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_ref_exists?("refs/heads/#{name}")
- else
- rugged_branch_exists?(name)
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_exists?("refs/heads/#{name}")
end
end
@@ -311,12 +266,8 @@ module Gitlab
end
def delete_all_refs_except(prefixes)
- gitaly_migrate(:ref_delete_refs) do |is_enabled|
- if is_enabled
- gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
- else
- delete_refs(*all_ref_names_except(prefixes))
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.delete_refs(except_with_prefixes: prefixes)
end
end
@@ -713,33 +664,18 @@ module Gitlab
Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
- def create_commit(params = {})
- params[:message].delete!("\r")
-
- Rugged::Commit.create(rugged, params)
- end
-
# Delete the specified branch from the repository
def delete_branch(branch_name)
- gitaly_migrate(:delete_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_ref_client.delete_branch(branch_name)
- else
- rugged.branches.delete(branch_name)
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.delete_branch(branch_name)
end
- rescue Rugged::ReferenceError, CommandError => e
+ rescue CommandError => e
raise DeleteBranchError, e
end
def delete_refs(*ref_names)
- gitaly_migrate(:delete_refs,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_delete_refs(*ref_names)
- else
- git_delete_refs(*ref_names)
- end
+ wrapped_gitaly_errors do
+ gitaly_delete_refs(*ref_names)
end
end
@@ -749,12 +685,8 @@ module Gitlab
# create_branch("feature")
# create_branch("other-feature", "master")
def create_branch(ref, start_point = "HEAD")
- gitaly_migrate(:create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_ref_client.create_branch(ref, start_point)
- else
- rugged_create_branch(ref, start_point)
- end
+ wrapped_gitaly_errors do
+ gitaly_ref_client.create_branch(ref, start_point)
end
end
@@ -1182,7 +1114,7 @@ module Gitlab
end
def can_be_merged?(source_sha, target_branch)
- if target_sha = find_branch(target_branch, true)&.target
+ if target_sha = find_branch(target_branch)&.target
!gitaly_conflicts_client(source_sha, target_sha).conflicts?
else
false
@@ -1556,52 +1488,10 @@ module Gitlab
# Returns true if the given ref name exists
#
# Ref names must start with `refs/`.
- def rugged_ref_exists?(ref_name)
- raise ArgumentError, 'invalid refname' unless ref_name.start_with?('refs/')
-
- rugged.references.exist?(ref_name)
- rescue Rugged::ReferenceError
- false
- end
-
- # Returns true if the given ref name exists
- #
- # Ref names must start with `refs/`.
def gitaly_ref_exists?(ref_name)
gitaly_ref_client.ref_exists?(ref_name)
end
- # Returns true if the given tag exists
- #
- # name - The name of the tag as a String.
- def rugged_tag_exists?(name)
- !!rugged.tags[name]
- end
-
- # Returns true if the given branch exists
- #
- # name - The name of the branch as a String.
- def rugged_branch_exists?(name)
- rugged.branches.exists?(name)
-
- # If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
- # Whatever code calls this method shouldn't have to deal with that so
- # instead we just return `false` (which is true since a branch doesn't
- # exist when it has an invalid name).
- rescue Rugged::ReferenceError
- false
- end
-
- def rugged_create_branch(ref, start_point)
- rugged_ref = rugged.branches.create(ref, start_point)
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
- rescue Rugged::ReferenceError => e
- raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ %r{'refs/heads/#{ref}'}
-
- raise InvalidRef.new("Invalid reference #{start_point}")
- end
-
def gitaly_copy_gitattributes(revision)
gitaly_repository_client.apply_gitattributes(revision)
end
@@ -1694,20 +1584,6 @@ module Gitlab
remote_update(remote_name, url: url)
end
- def git_delete_refs(*ref_names)
- instructions = ref_names.map do |ref|
- "delete #{ref}\x00\x00"
- end
-
- message, status = run_git(%w[update-ref --stdin -z]) do |stdin|
- stdin.write(instructions.join)
- end
-
- unless status.zero?
- raise GitError.new("Could not delete refs #{ref_names}: #{message}")
- end
- end
-
def gitaly_delete_refs(*ref_names)
gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
end
@@ -1758,6 +1634,12 @@ module Gitlab
def sha_from_ref(ref)
rev_parse_target(ref).oid
end
+
+ def create_commit(params = {})
+ params[:message].delete!("\r")
+
+ Rugged::Commit.create(rugged, params)
+ end
end
end
end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 5fdad077eea..2ba68343aa5 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -12,35 +12,12 @@ module Gitlab
end
# This method returns an array of new commit references
- def new_refs
- repository.rev_list(including: newrev, excluding: :all).split("\n")
- end
-
- # Finds newly added objects
- # Returns an array of shas
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233
#
- # Can skip objects which do not have a path using required_path: true
- # This skips commit objects and root trees, which might not be needed when
- # looking for blobs
- #
- # When given a block it will yield objects as a lazy enumerator so
- # the caller can limit work done instead of processing megabytes of data
- def new_objects(options: [], require_path: nil, not_in: nil, &lazy_block)
- opts = {
- including: newrev,
- options: options,
- excluding: not_in.nil? ? :all : not_in,
- require_path: require_path
- }
-
- get_objects(opts, &lazy_block)
- end
-
- def all_objects(options: [], require_path: nil, &lazy_block)
- get_objects(including: :all,
- options: options,
- require_path: require_path,
- &lazy_block)
+ def new_refs
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rev_list(including: newrev, excluding: :all).split("\n")
+ end
end
private
diff --git a/lib/gitlab/gitaly_client/server_service.rb b/lib/gitlab/gitaly_client/server_service.rb
index 2e1076d1f66..ad898278353 100644
--- a/lib/gitlab/gitaly_client/server_service.rb
+++ b/lib/gitlab/gitaly_client/server_service.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def info
- GitalyClient.call(@storage, :server_service, :server_info, Gitaly::ServerInfoRequest.new)
+ GitalyClient.call(@storage, :server_service, :server_info, Gitaly::ServerInfoRequest.new, timeout: GitalyClient.fast_timeout)
end
end
end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 60d5fa4d29a..af9b880ef9e 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -16,7 +16,8 @@ module Gitlab
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),
ImportSource.new('git', 'Repo by URL', nil),
ImportSource.new('gitlab_project', 'GitLab export', Gitlab::ImportExport::Importer),
- ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer)
+ ImportSource.new('gitea', 'Gitea', Gitlab::LegacyGithubImport::Importer),
+ ImportSource.new('manifest', 'Manifest file', nil)
].freeze
class << self
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index da43bd0af4b..15c5ece2350 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -1,6 +1,10 @@
module Gitlab
# Helper methods to do with Kubernetes network services & resources
module Kubernetes
+ def self.build_header_hash
+ Hash.new { |h, k| h[k] = [] }
+ end
+
# This is the comand that is run to start a terminal session. Kubernetes
# expects `command=foo&command=bar, not `command[]=foo&command[]=bar`
EXEC_COMMAND = URI.encode_www_form(
@@ -37,13 +41,14 @@ module Gitlab
selectors: { pod: pod_name, container: container["name"] },
url: container_exec_url(api_url, namespace, pod_name, container["name"]),
subprotocols: ['channel.k8s.io'],
- headers: Hash.new { |h, k| h[k] = [] },
+ headers: ::Gitlab::Kubernetes.build_header_hash,
created_at: created_at
}
end
end
def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil)
+ terminal[:headers] ||= ::Gitlab::Kubernetes.build_header_hash
terminal[:headers]['Authorization'] << "Bearer #{token}"
terminal[:max_session_time] = max_session_time
terminal[:ca_pem] = ca_pem if ca_pem.present?
diff --git a/lib/gitlab/manifest_import/manifest.rb b/lib/gitlab/manifest_import/manifest.rb
new file mode 100644
index 00000000000..4d6034fb956
--- /dev/null
+++ b/lib/gitlab/manifest_import/manifest.rb
@@ -0,0 +1,81 @@
+# Class to parse manifest file and build a list of repositories for import
+#
+# <manifest>
+# <remote review="https://android-review.googlesource.com/" />
+# <project path="platform-common" name="platform" />
+# <project path="platform/art" name="platform/art" />
+# <project path="platform/device" name="platform/device" />
+# </manifest>
+#
+# 1. Project path must be uniq and can't be part of other project path.
+# For example, you can't have projects with 'foo' and 'foo/bar' paths.
+# 2. Remote must be present with review attribute so GitLab knows
+# where to fetch source code
+module Gitlab
+ module ManifestImport
+ class Manifest
+ attr_reader :parsed_xml, :errors
+
+ def initialize(file)
+ @parsed_xml = Nokogiri::XML(file) { |config| config.strict }
+ @errors = []
+ rescue Nokogiri::XML::SyntaxError
+ @errors = ['The uploaded file is not a valid XML file.']
+ end
+
+ def projects
+ raw_projects.each_with_index.map do |project, i|
+ {
+ id: i,
+ name: project['name'],
+ path: project['path'],
+ url: repository_url(project['name'])
+ }
+ end
+ end
+
+ def valid?
+ return false if @errors.any?
+
+ unless validate_remote
+ @errors << 'Make sure a <remote> tag is present and is valid.'
+ end
+
+ unless validate_projects
+ @errors << 'Make sure every <project> tag has name and path attributes.'
+ end
+
+ @errors.empty?
+ end
+
+ private
+
+ def validate_remote
+ remote.present? && URI.parse(remote).host
+ rescue URI::Error
+ false
+ end
+
+ def validate_projects
+ raw_projects.all? do |project|
+ project['name'] && project['path']
+ end
+ end
+
+ def repository_url(name)
+ URI.join(remote, name).to_s
+ end
+
+ def remote
+ return @remote if defined?(@remote)
+
+ remote_tag = parsed_xml.css('manifest > remote').first
+ @remote = remote_tag['review'] if remote_tag
+ end
+
+ def raw_projects
+ @raw_projects ||= parsed_xml.css('manifest > project')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/manifest_import/project_creator.rb b/lib/gitlab/manifest_import/project_creator.rb
new file mode 100644
index 00000000000..b5967c93735
--- /dev/null
+++ b/lib/gitlab/manifest_import/project_creator.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module ManifestImport
+ class ProjectCreator
+ attr_reader :repository, :destination, :current_user
+
+ def initialize(repository, destination, current_user)
+ @repository = repository
+ @destination = destination
+ @current_user = current_user
+ end
+
+ def execute
+ group_full_path, _, project_path = repository[:path].rpartition('/')
+ group_full_path = File.join(destination.full_path, group_full_path) if destination
+ group = create_group_with_parents(group_full_path)
+
+ params = {
+ import_url: repository[:url],
+ import_type: 'manifest',
+ namespace_id: group.id,
+ path: project_path,
+ name: project_path,
+ visibility_level: destination.visibility_level
+ }
+
+ Projects::CreateService.new(current_user, params).execute
+ end
+
+ private
+
+ def create_group_with_parents(full_path)
+ params = {
+ group_path: full_path,
+ visibility_level: destination.visibility_level
+ }
+
+ Groups::NestedCreateService.new(current_user, params).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index b9832a724c4..d0cb7c1a7cf 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -34,11 +34,16 @@ module Gitlab
start = Time.now
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
+ # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
+ out_reader = Thread.new { stdout.read }
+ err_reader = Thread.new { stderr.read }
+
yield(stdin) if block_given?
stdin.close
- cmd_stdout = stdout.read
- cmd_stderr = stderr.read
+ cmd_stdout = out_reader.value
+ cmd_stderr = err_reader.value
cmd_status = wait_thr.value
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 83ff735580e..ab488218288 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -77,6 +77,9 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
+msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
+msgstr ""
+
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
@@ -202,6 +205,9 @@ msgstr ""
msgid "404|Please contact your GitLab administrator if you think this is a mistake."
msgstr ""
+msgid "<strong>%{group_name}</strong> group members"
+msgstr ""
+
msgid "<strong>Removes</strong> source branch"
msgstr ""
@@ -283,6 +289,12 @@ msgstr ""
msgid "Add todo"
msgstr ""
+msgid "Add user(s) to the group:"
+msgstr ""
+
+msgid "Add users to group"
+msgstr ""
+
msgid "AdminArea|Stop all jobs"
msgstr ""
@@ -463,6 +475,9 @@ msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
+msgid "Any"
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -481,6 +496,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
+msgid "Are you sure you want to remove %{group_name}?"
+msgstr ""
+
msgid "Are you sure you want to remove this identity?"
msgstr ""
@@ -685,6 +703,9 @@ msgstr ""
msgid "Below are examples of regex for existing tools:"
msgstr ""
+msgid "Below you will find all the groups that are public."
+msgstr ""
+
msgid "Boards"
msgstr ""
@@ -975,6 +996,9 @@ msgstr ""
msgid "Choose file..."
msgstr ""
+msgid "Choose the top-level group for your repository imports."
+msgstr ""
+
msgid "Choose which repositories you want to import."
msgstr ""
@@ -1657,9 +1681,6 @@ msgstr ""
msgid "Copy commit SHA to clipboard"
msgstr ""
-msgid "Copy file name to clipboard"
-msgstr ""
-
msgid "Copy file path to clipboard"
msgstr ""
@@ -1699,6 +1720,9 @@ msgstr ""
msgid "Create file"
msgstr ""
+msgid "Create group"
+msgstr ""
+
msgid "Create group label"
msgstr ""
@@ -1720,6 +1744,9 @@ msgstr ""
msgid "Create new file"
msgstr ""
+msgid "Create new file or directory"
+msgstr ""
+
msgid "Create new label"
msgstr ""
@@ -1744,6 +1771,9 @@ msgstr ""
msgid "Created by me"
msgstr ""
+msgid "Created on:"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -1950,6 +1980,9 @@ msgstr ""
msgid "Description"
msgstr ""
+msgid "Description:"
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -1977,6 +2010,12 @@ msgstr ""
msgid "Discard draft"
msgstr ""
+msgid "Discover projects, groups and snippets. Share your projects with others"
+msgstr ""
+
+msgid "Dismiss"
+msgstr ""
+
msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
@@ -2040,6 +2079,9 @@ msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
+msgid "Edit group: %{group_name}"
+msgstr ""
+
msgid "Edit identity for %{user_name}"
msgstr ""
@@ -2247,6 +2289,12 @@ msgstr ""
msgid "Expand sidebar"
msgstr ""
+msgid "Explore GitLab"
+msgstr ""
+
+msgid "Explore Groups"
+msgstr ""
+
msgid "Explore groups"
msgstr ""
@@ -2426,18 +2474,33 @@ msgstr ""
msgid "Graph"
msgstr ""
+msgid "Group"
+msgstr ""
+
msgid "Group CI/CD settings"
msgstr ""
+msgid "Group Git LFS status:"
+msgstr ""
+
msgid "Group ID"
msgstr ""
msgid "Group Runners"
msgstr ""
+msgid "Group avatar"
+msgstr ""
+
+msgid "Group info:"
+msgstr ""
+
msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
+msgid "Group: %{group_name}"
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -2462,6 +2525,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -2626,6 +2692,9 @@ msgstr ""
msgid "Import in progress"
msgstr ""
+msgid "Import multiple repositories by uploading a manifest file."
+msgstr ""
+
msgid "Import repositories from GitHub"
msgstr ""
@@ -2835,6 +2904,9 @@ msgstr ""
msgid "List"
msgstr ""
+msgid "List available repositories"
+msgstr ""
+
msgid "List your GitHub repositories"
msgstr ""
@@ -2862,6 +2934,9 @@ msgstr ""
msgid "Locked to current projects"
msgstr ""
+msgid "Manage access"
+msgstr ""
+
msgid "Manage all notifications"
msgstr ""
@@ -2874,6 +2949,12 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
+msgid "Manifest"
+msgstr ""
+
+msgid "Manifest file import"
+msgstr ""
+
msgid "Mar"
msgstr ""
@@ -3024,6 +3105,9 @@ msgstr ""
msgid "More information is available|here"
msgstr ""
+msgid "Most stars"
+msgstr ""
+
msgid "Move"
msgstr ""
@@ -3039,6 +3123,9 @@ msgstr ""
msgid "Name your individual key via a title"
msgstr ""
+msgid "Name:"
+msgstr ""
+
msgid "Nav|Help"
msgstr ""
@@ -3054,6 +3141,9 @@ msgstr ""
msgid "New"
msgstr ""
+msgid "New Group"
+msgstr ""
+
msgid "New Identity"
msgstr ""
@@ -3152,6 +3242,9 @@ msgstr ""
msgid "No messages were logged"
msgstr ""
+msgid "No public groups"
+msgstr ""
+
msgid "No repository"
msgstr ""
@@ -3338,6 +3431,9 @@ msgstr ""
msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
msgstr ""
+msgid "Path:"
+msgstr ""
+
msgid "Pause"
msgstr ""
@@ -3698,6 +3794,9 @@ msgstr ""
msgid "Projects"
msgstr ""
+msgid "Projects shared with %{group_name}"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
@@ -3824,6 +3923,9 @@ msgstr ""
msgid "Read more"
msgstr ""
+msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
+msgstr ""
+
msgid "Readme"
msgstr ""
@@ -3890,6 +3992,9 @@ msgstr ""
msgid "Repository Settings"
msgstr ""
+msgid "Repository URL"
+msgstr ""
+
msgid "Repository maintenance"
msgstr ""
@@ -4380,6 +4485,9 @@ msgstr ""
msgid "Storage"
msgstr ""
+msgid "Storage:"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -4935,6 +5043,9 @@ msgstr ""
msgid "Track time with quick actions"
msgstr ""
+msgid "Trending"
+msgstr ""
+
msgid "Trigger this manual action"
msgstr ""
@@ -5075,6 +5186,12 @@ msgstr ""
msgid "Visibility and access controls"
msgstr ""
+msgid "Visibility level:"
+msgstr ""
+
+msgid "Visibility:"
+msgstr ""
+
msgid "VisibilityLevel|Internal"
msgstr ""
@@ -5279,6 +5396,9 @@ msgstr ""
msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}"
msgstr ""
+msgid "You can easily contribute to them by requesting to join these groups."
+msgstr ""
+
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
@@ -5422,6 +5542,9 @@ msgstr ""
msgid "for this project"
msgstr ""
+msgid "here"
+msgstr ""
+
msgid "importing"
msgstr ""
diff --git a/qa/README.md b/qa/README.md
index a4b4398645e..be4cf89ebbc 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -55,7 +55,7 @@ Since the arguments would be passed to `rspec`, you could use all `rspec`
options there. For example, passing `--backtrace` and also line number:
```
-bin/qa Test::Instance http://localhost qa/specs/features/login/standard_spec.rb:3 --backtrace
+bin/qa Test::Instance http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace
```
### Overriding the authenticated user
diff --git a/qa/qa.rb b/qa/qa.rb
index be7bcf7a2ad..ef83722de90 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -1,5 +1,7 @@
$: << File.expand_path(File.dirname(__FILE__))
+Encoding.default_external = 'UTF-8'
+
module QA
##
# GitLab QA runtime classes, mostly singletons.
@@ -41,14 +43,17 @@ module QA
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github'
+ autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :Branch, 'qa/factory/resource/branch'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
autoload :Runner, 'qa/factory/resource/runner'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster'
+ autoload :User, 'qa/factory/resource/user'
autoload :ProjectMilestone, 'qa/factory/resource/project_milestone'
autoload :Wiki, 'qa/factory/resource/wiki'
+ autoload :Fork, 'qa/factory/resource/fork'
end
module Repository
@@ -107,6 +112,7 @@ module QA
module Main
autoload :Login, 'qa/page/main/login'
autoload :OAuth, 'qa/page/main/oauth'
+ autoload :SignUp, 'qa/page/main/sign_up'
end
module Settings
@@ -167,6 +173,10 @@ module QA
autoload :Index, 'qa/page/project/issue/index'
end
+ module Fork
+ autoload :New, 'qa/page/project/fork/new'
+ end
+
module Milestone
autoload :New, 'qa/page/project/milestone/new'
autoload :Index, 'qa/page/project/milestone/index'
@@ -200,6 +210,10 @@ module QA
autoload :Sidebar, 'qa/page/issuable/sidebar'
end
+ module Layout
+ autoload :Banner, 'qa/page/layout/banner'
+ end
+
module MergeRequest
autoload :New, 'qa/page/merge_request/new'
autoload :Show, 'qa/page/merge_request/show'
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb
index 48674c08a8d..4f78098d348 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/factory/repository/project_push.rb
@@ -11,6 +11,8 @@ module QA
factory.output
end
+ product(:project) { |factory| factory.project }
+
def initialize
@file_name = 'file.txt'
@file_content = '# This is test project'
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 4f97e65b091..5b7ebf6c41f 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -5,7 +5,8 @@ module QA
module Repository
class Push < Factory::Base
attr_accessor :file_name, :file_content, :commit_message,
- :branch_name, :new_branch, :output, :repository_uri
+ :branch_name, :new_branch, :output, :repository_uri,
+ :user
attr_writer :remote_branch
@@ -31,9 +32,20 @@ module QA
def fabricate!
Git::Repository.perform do |repository|
repository.uri = repository_uri
+
repository.use_default_credentials
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ if user
+ repository.username = user.username
+ repository.password = user.password
+ username = user.name
+ email = user.email
+ end
+
repository.clone
- repository.configure_identity('GitLab QA', 'root@gitlab.com')
+ repository.configure_identity(username, email)
if new_branch
repository.checkout_new_branch(branch_name)
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
new file mode 100644
index 00000000000..1d0c76a3d30
--- /dev/null
+++ b/qa/qa/factory/resource/fork.rb
@@ -0,0 +1,24 @@
+module QA
+ module Factory
+ module Resource
+ class Fork < Factory::Base
+ dependency Factory::Repository::ProjectPush, as: :push
+
+ dependency Factory::Resource::User, as: :user
+
+ product(:user) { |factory| factory.user }
+
+ def fabricate!
+ push.project.visit!
+ Page::Project::Show.act { fork_project }
+
+ Page::Project::Fork::New.perform do |fork_new|
+ fork_new.choose_namespace(user.name)
+ end
+
+ Page::Layout::Banner.act { has_notice?('The project was successfully forked.') }
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb
index f32cf985e9d..1c9e5f94b22 100644
--- a/qa/qa/factory/resource/kubernetes_cluster.rb
+++ b/qa/qa/factory/resource/kubernetes_cluster.rb
@@ -36,6 +36,9 @@ module QA
if @install_helm_tiller
Page::Project::Operations::Kubernetes::Show.perform do |page|
+ # We must wait a few seconds for permissions to be setup correctly for new cluster
+ sleep 10
+
# Helm must be installed before everything else
page.install!(:helm)
page.await_installed(:helm)
diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb
new file mode 100644
index 00000000000..6caaf65f673
--- /dev/null
+++ b/qa/qa/factory/resource/merge_request_from_fork.rb
@@ -0,0 +1,24 @@
+module QA
+ module Factory
+ module Resource
+ class MergeRequestFromFork < MergeRequest
+ attr_accessor :fork_branch
+
+ dependency Factory::Resource::Fork, as: :fork
+
+ dependency Factory::Repository::ProjectPush, as: :push do |push, factory|
+ push.project = factory.fork
+ push.branch_name = factory.fork_branch
+ push.file_name = 'file2.txt'
+ push.user = factory.fork.user
+ end
+
+ def fabricate!
+ fork.visit!
+ Page::Project::Show.act { new_merge_request }
+ Page::MergeRequest::New.act { create_merge_request }
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index 7bc64c6ae5d..7fff22b5468 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -37,6 +37,7 @@ module QA
page.choose_test_namespace
page.choose_name(@name)
page.add_description(@description)
+ page.set_visibility('Public')
page.create_new_project
end
end
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
new file mode 100644
index 00000000000..e08df9e0cd0
--- /dev/null
+++ b/qa/qa/factory/resource/user.rb
@@ -0,0 +1,34 @@
+require 'securerandom'
+
+module QA
+ module Factory
+ module Resource
+ class User < Factory::Base
+ attr_accessor :name, :username, :email, :password
+
+ def initialize
+ @name = "name-#{SecureRandom.hex(8)}"
+ @username = "username-#{SecureRandom.hex(8)}"
+ @email = "mail#{SecureRandom.hex(8)}@mail.com"
+ @password = 'password'
+ end
+
+ product(:name) { |factory| factory.name }
+
+ product(:username) { |factory| factory.username }
+
+ product(:email) { |factory| factory.email }
+
+ product(:password) { |factory| factory.password }
+
+ def fabricate!
+ Page::Menu::Main.act { sign_out }
+ Page::Main::Login.act { switch_to_register_tab }
+ Page::Main::SignUp.perform do |page|
+ page.sign_up!(name: name, username: username, email: email, password: password)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/issuable/sidebar.rb b/qa/qa/page/issuable/sidebar.rb
index dec2ce1eab3..f207264e24f 100644
--- a/qa/qa/page/issuable/sidebar.rb
+++ b/qa/qa/page/issuable/sidebar.rb
@@ -4,6 +4,7 @@ module QA
class Sidebar < Page::Base
view 'app/views/shared/issuable/_sidebar.html.haml' do
element :labels_block, ".issuable-show-labels"
+ element :milestones_block, '.block.milestone'
end
def has_label?(label)
@@ -11,6 +12,12 @@ module QA
!!find('span', text: label)
end
end
+
+ def has_milestone?(milestone)
+ page.within('.block.milestone') do
+ !!find("[href*='/milestones/']", text: milestone)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/layout/banner.rb b/qa/qa/page/layout/banner.rb
new file mode 100644
index 00000000000..e7654bdafc9
--- /dev/null
+++ b/qa/qa/page/layout/banner.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Layout
+ class Banner < Page::Base
+ view 'app/views/layouts/header/_read_only_banner.html.haml' do
+ element :flash_notice, ".flash-notice"
+ end
+
+ def has_notice?(message)
+ page.within('.flash-notice') do
+ !!find('span', text: message)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 26c99efc53d..6cdfbd1c125 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -25,19 +25,24 @@ module QA
element :standard_tab, "link_to 'Standard'"
end
+ view 'app/views/devise/shared/_tabs_normal.html.haml' do
+ element :sign_in_tab, /nav-link.*login-pane.*Sign in/
+ element :register_tab, /nav-link.*register-pane.*Register/
+ end
+
def initialize
# The login page is usually the entry point for all the scenarios so
# we need to wait for the instance to start. That said, in some cases
# we are already logged-in so we check both cases here.
wait(max: 500) do
page.has_css?('.login-page') ||
- Page::Menu::Main.act { has_personal_area? }
+ Page::Menu::Main.act { has_personal_area?(wait: 0) }
end
end
def sign_in_using_credentials
# Don't try to log-in if we're already logged-in
- return if Page::Menu::Main.act { has_personal_area? }
+ return if Page::Menu::Main.act { has_personal_area?(wait: 0) }
using_wait_time 0 do
set_initial_password_if_present
@@ -48,12 +53,22 @@ module QA
sign_in_using_gitlab_credentials
end
end
+
+ Page::Menu::Main.act { has_personal_area? }
end
def self.path
'/users/sign_in'
end
+ def switch_to_sign_in_tab
+ click_on 'Sign in'
+ end
+
+ def switch_to_register_tab
+ click_on 'Register'
+ end
+
private
def sign_in_using_ldap_credentials
diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb
new file mode 100644
index 00000000000..9a834e94b81
--- /dev/null
+++ b/qa/qa/page/main/sign_up.rb
@@ -0,0 +1,27 @@
+module QA
+ module Page
+ module Main
+ class SignUp < Page::Base
+ view 'app/views/devise/shared/_signup_box.html.haml' do
+ element :name, 'text_field :name'
+ element :username, 'text_field :username'
+ element :email_field, 'email_field :email'
+ element :email_confirmation, 'email_field :email_confirmation'
+ element :password, 'password_field :password'
+ element :register_button, 'submit "Register"'
+ end
+
+ def sign_up!(name:, username:, email:, password:)
+ fill_in :new_user_name, with: name
+ fill_in :new_user_username, with: username
+ fill_in :new_user_email, with: email
+ fill_in :new_user_email_confirmation, with: email
+ fill_in :new_user_password, with: password
+ click_button 'Register'
+
+ Page::Menu::Main.act { has_personal_area? }
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb
index aef5c9f9c82..36e7285f7b7 100644
--- a/qa/qa/page/menu/main.rb
+++ b/qa/qa/page/menu/main.rb
@@ -60,9 +60,9 @@ module QA
end
end
- def has_personal_area?
+ def has_personal_area?(wait: Capybara.default_max_wait_time)
# No need to wait, either we're logged-in, or not.
- using_wait_time(0) { page.has_selector?('.qa-user-avatar') }
+ using_wait_time(wait) { page.has_selector?('.qa-user-avatar') }
end
private
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index f3200160a78..c200f14f4fb 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -79,12 +79,6 @@ module QA
click_element :squash_checkbox
end
-
- def has_milestone?(milestone_title)
- page.within('.issuable-sidebar') do
- !!find("[href*='/milestones/']", text: milestone_title, wait: 1)
- end
- end
end
end
end
diff --git a/qa/qa/page/project/fork/new.rb b/qa/qa/page/project/fork/new.rb
new file mode 100644
index 00000000000..ed92df956bf
--- /dev/null
+++ b/qa/qa/page/project/fork/new.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Project
+ module Fork
+ class New < Page::Base
+ view 'app/views/projects/forks/_fork_button.html.haml' do
+ element :namespace, 'link_to project_forks_path'
+ end
+
+ def choose_namespace(namespace = Runtime::Namespace.path)
+ click_on namespace
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 7976e96d43b..9e812fa7c74 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -14,6 +14,7 @@ module QA
element :project_path, 'text_field :path'
element :project_description, 'text_area :description'
element :project_create_button, "submit 'Create project'"
+ element :visibility_radios, 'visibility_level:'
end
view 'app/views/projects/_import_project_pane.html.haml' do
@@ -42,6 +43,10 @@ module QA
click_on 'Create project'
end
+ def set_visibility(visibility)
+ choose visibility
+ end
+
def go_to_github_import
click_link 'GitHub'
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 1dcdb59490a..88861d5772d 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -22,6 +22,11 @@ module QA
element :branches_dropdown
end
+ view 'app/views/projects/buttons/_fork.html.haml' do
+ element :fork_label, "%span= s_('GoToYourFork|Fork')"
+ element :fork_link, "link_to new_project_fork_path(@project)"
+ end
+
view 'app/views/projects/_files.html.haml' do
element :tree_holder, '.tree-holder'
end
@@ -61,6 +66,10 @@ module QA
click_link 'New issue'
end
+
+ def fork_project
+ click_on 'Fork'
+ end
end
end
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index cee381f3379..877864fb40c 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -85,6 +85,10 @@ module QA
driver.browser.save_screenshot(path)
end
+ Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example|
+ File.join(QA::Runtime::Namespace.name, example.file_path.sub('./qa/specs/features/', ''))
+ end
+
Capybara.configure do |config|
config.default_driver = :chrome
config.javascript_driver = :chrome
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index ccfa8b44db3..f1c8ef11f94 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -8,7 +8,7 @@ module QA
end
def name
- 'qa-test-' + time.strftime('%d-%m-%Y-%H-%M-%S')
+ "qa-test-#{time.strftime('%Y-%m-%d-%H-%M-%S')}"
end
def path
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
deleted file mode 100644
index 254f47cf217..00000000000
--- a/qa/qa/specs/features/login/standard_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module QA
- describe 'standard user login', :core do
- it 'user logs in using credentials' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
-
- # TODO, since `Signed in successfully` message was removed
- # this is the only way to tell if user is signed in correctly.
- #
- Page::Menu::Main.perform do |menu|
- expect(menu).to have_personal_area
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb
index 5807e539699..36d7efb02e1 100644
--- a/qa/qa/specs/features/merge_request/create_spec.rb
+++ b/qa/qa/specs/features/merge_request/create_spec.rb
@@ -20,11 +20,12 @@ module QA
merge_request.milestone = current_milestone
end
- Page::MergeRequest::Show.perform do |merge_request|
- expect(page).to have_content('This is a merge request with a milestone')
- expect(page).to have_content('Great feature with milestone')
- expect(page).to have_content(/Opened [\w\s]+ ago/)
- expect(merge_request).to have_milestone(current_milestone.title)
+ expect(page).to have_content('This is a merge request with a milestone')
+ expect(page).to have_content('Great feature with milestone')
+ expect(page).to have_content(/Opened [\w\s]+ ago/)
+
+ Page::Issuable::Sidebar.perform do |sidebar|
+ expect(sidebar).to have_milestone(current_milestone.title)
end
end
end
diff --git a/qa/qa/specs/features/project/fork_project_spec.rb b/qa/qa/specs/features/project/fork_project_spec.rb
new file mode 100644
index 00000000000..8ad0120305a
--- /dev/null
+++ b/qa/qa/specs/features/project/fork_project_spec.rb
@@ -0,0 +1,23 @@
+module QA
+ describe 'Project fork', :core do
+ it 'can submit merge requests to upstream master' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request|
+ merge_request.fork_branch = 'feature-branch'
+ end
+
+ Page::Menu::Main.act { sign_out }
+ Page::Main::Login.act do
+ switch_to_sign_in_tab
+ sign_in_using_credentials
+ end
+
+ merge_request.visit!
+ Page::MergeRequest::Show.act { merge! }
+
+ expect(page).to have_content('The changes were merged')
+ end
+ end
+end
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
index 29ea2e69ec7..4e593a69aae 100644
--- a/qa/qa/specs/features/repository/protected_branches_spec.rb
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -13,15 +13,11 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
end
- after do |example|
+ after do
# We need to clear localStorage because we're using it for the dropdown,
# and capybara doesn't do this for us.
# https://github.com/teamcapybara/capybara/issues/1702
Capybara.execute_script 'localStorage.clear()'
-
- # In order to help diagnose a false failure
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/48241
- log_push_output if example.exception
end
context 'when developers and maintainers are allowed to push to a protected branch' do
@@ -31,9 +27,9 @@ module QA
expect(protected_branch.name).to have_content(branch_name)
expect(protected_branch.push_allowance).to have_content('Developers + Maintainers')
- @push = push_new_file(branch_name)
+ push = push_new_file(branch_name)
- expect(@push.output).to match(/remote: To create a merge request for protected-branch, visit/)
+ expect(push.output).to match(/remote: To create a merge request for protected-branch, visit/)
end
end
@@ -41,11 +37,11 @@ module QA
it 'user without push rights fails to push to the protected branch' do
create_protected_branch(allow_to_push: false)
- @push = push_new_file(branch_name)
+ push = push_new_file(branch_name)
- expect(@push.output)
+ expect(push.output)
.to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
- expect(@push.output)
+ expect(push.output)
.to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
end
end
@@ -69,13 +65,5 @@ module QA
resource.new_branch = false
end
end
-
- def log_push_output
- if defined?(@push)
- filename = File.join('tmp', "push-output-#{project.name}")
- puts "Exception detected. Push output will be saved to #{filename}"
- IO.binwrite(filename, @push.output)
- end
- end
end
end
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index f278043028f..9dc4edf97d1 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -3,6 +3,20 @@ require 'spec_helper'
load File.expand_path('../../bin/changelog', __dir__)
describe 'bin/changelog' do
+ let(:options) { OpenStruct.new(title: 'Test title', type: 'fixed', dry_run: true) }
+
+ describe ChangelogEntry do
+ it 'truncates the file path' do
+ entry = described_class.new(options)
+
+ allow(entry).to receive(:ee?).and_return(false)
+ allow(entry).to receive(:branch_name).and_return('long-branch-' * 100)
+
+ file_path = entry.send(:file_path)
+ expect(file_path.length).to eq(140)
+ end
+ end
+
describe ChangelogOptionParser do
describe '.parse' do
it 'parses --amend' do
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 7376841fac8..c7c83369d7c 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -15,55 +15,16 @@ describe MetricsController do
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(metrics_multiproc_dir)
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(true)
allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip, whitelisted_ip_range])
+ allow_any_instance_of(MetricsService).to receive(:metrics_text).and_return("prometheus_counter 1")
end
describe '#index' do
shared_examples_for 'endpoint providing metrics' do
- it 'returns DB ping metrics' do
+ it 'returns prometheus metrics' do
get :index
- expect(response.body).to match(/^db_ping_timeout 0$/)
- expect(response.body).to match(/^db_ping_success 1$/)
- expect(response.body).to match(/^db_ping_latency_seconds [0-9\.]+$/)
- end
-
- it 'returns Redis ping metrics' do
- get :index
-
- expect(response.body).to match(/^redis_ping_timeout 0$/)
- expect(response.body).to match(/^redis_ping_success 1$/)
- expect(response.body).to match(/^redis_ping_latency_seconds [0-9\.]+$/)
- end
-
- it 'returns Caching ping metrics' do
- get :index
-
- expect(response.body).to match(/^redis_cache_ping_timeout 0$/)
- expect(response.body).to match(/^redis_cache_ping_success 1$/)
- expect(response.body).to match(/^redis_cache_ping_latency_seconds [0-9\.]+$/)
- end
-
- it 'returns Queues ping metrics' do
- get :index
-
- expect(response.body).to match(/^redis_queues_ping_timeout 0$/)
- expect(response.body).to match(/^redis_queues_ping_success 1$/)
- expect(response.body).to match(/^redis_queues_ping_latency_seconds [0-9\.]+$/)
- end
-
- it 'returns SharedState ping metrics' do
- get :index
-
- expect(response.body).to match(/^redis_shared_state_ping_timeout 0$/)
- expect(response.body).to match(/^redis_shared_state_ping_success 1$/)
- expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/)
- end
-
- it 'returns Gitaly metrics' do
- get :index
-
- expect(response.body).to match(/^gitaly_health_check_success{shard="default"} 1$/)
- expect(response.body).to match(/^gitaly_health_check_latency_seconds{shard="default"} [0-9\.]+$/)
+ expect(response.status).to eq(200)
+ expect(response.body).to match(/^prometheus_counter 1$/)
end
context 'prometheus metrics are disabled' do
@@ -101,7 +62,7 @@ describe MetricsController do
allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip)
end
- it 'returns proper response' do
+ it 'returns the expected error response' do
get :index
expect(response.status).to eq(404)
diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index 58f2817c7cc..1ce7e84bef9 100644
--- a/spec/controllers/projects/todos_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -5,29 +5,10 @@ describe Projects::TodosController do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project) }
- let(:parent) { project }
-
- shared_examples 'project todos actions' do
- it_behaves_like 'todos actions'
-
- context 'when not authorized for resource' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
- project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
- sign_in(user)
- end
-
- it "doesn't create todo" do
- expect { post_create }.not_to change { user.todos.count }
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
context 'Issues' do
describe 'POST create' do
- def post_create
+ def go
post :create,
namespace_id: project.namespace,
project_id: project,
@@ -36,13 +17,66 @@ describe Projects::TodosController do
format: 'html'
end
- it_behaves_like 'project todos actions'
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ it 'creates todo for issue' do
+ expect do
+ go
+ end.to change { user.todos.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'returns todo path and pending count' do
+ go
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['count']).to eq 1
+ expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}})
+ end
+ end
+
+ context 'when not authorized for project' do
+ it 'does not create todo for issue that user has no access to' do
+ sign_in(user)
+ expect do
+ go
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'does not create todo for issue when user not logged in' do
+ expect do
+ go
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when not authorized for issue' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ sign_in(user)
+ end
+
+ it "doesn't create todo" do
+ expect { go }.not_to change { user.todos.count }
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
end
context 'Merge Requests' do
describe 'POST create' do
- def post_create
+ def go
post :create,
namespace_id: project.namespace,
project_id: project,
@@ -51,7 +85,60 @@ describe Projects::TodosController do
format: 'html'
end
- it_behaves_like 'project todos actions'
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ it 'creates todo for merge request' do
+ expect do
+ go
+ end.to change { user.todos.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'returns todo path and pending count' do
+ go
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['count']).to eq 1
+ expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}})
+ end
+ end
+
+ context 'when not authorized for project' do
+ it 'does not create todo for merge request user has no access to' do
+ sign_in(user)
+ expect do
+ go
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'does not create todo for merge request user has no access to' do
+ expect do
+ go
+ end.to change { user.todos.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when not authorized for merge_request' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ sign_in(user)
+ end
+
+ it "doesn't create todo" do
+ expect { go }.not_to change { user.todos.count }
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 14486c80341..94f8caedfa6 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -1,8 +1,8 @@
FactoryBot.define do
factory :todo do
project
- author { project&.creator || user }
- user { project&.creator || user }
+ author { project.creator }
+ user { project.creator }
target factory: :issue
action { Todo::ASSIGNED }
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 053e3b189c3..e62bd6f8187 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -154,6 +154,12 @@ describe 'Group' do
end
end
+ it 'focuses confirmation field on remove group' do
+ click_button('Remove group')
+
+ expect(page).to have_selector '#confirm_name_input:focus'
+ end
+
it 'removes group' do
expect { remove_with_confirm('Remove group', group.path) }.to change {Group.count}.by(-1)
expect(group.members.all.count).to be_zero
diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb
new file mode 100644
index 00000000000..e381d073804
--- /dev/null
+++ b/spec/features/import/manifest_import_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'Import multiple repositories by uploading a manifest file', :js, :postgresql do
+ include Select2Helper
+
+ let(:user) { create(:admin) }
+ let(:group) { create(:group) }
+
+ before do
+ sign_in(user)
+
+ group.add_owner(user)
+ end
+
+ it 'parses manifest file and list repositories' do
+ visit new_import_manifest_path
+
+ attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
+ click_on 'List available repositories'
+
+ expect(page).to have_button('Import all repositories')
+ expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
+ end
+
+ it 'imports succesfully imports a project' do
+ visit new_import_manifest_path
+
+ attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
+ click_on 'List available repositories'
+
+ page.within(first_row) do
+ click_on 'Import'
+
+ expect(page).to have_content 'Done'
+ expect(page).to have_content("#{group.full_path}/build/make")
+ end
+ end
+
+ it 'renders an error if invalid file was provided' do
+ visit new_import_manifest_path
+
+ attach_file('manifest', Rails.root.join('spec/fixtures/banana_sample.gif'))
+ click_on 'List available repositories'
+
+ expect(page).to have_content 'The uploaded file is not a valid XML file.'
+ end
+
+ def first_row
+ page.all('table.import-jobs tbody tr')[0]
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
index b1b62d04ac2..441b080bee5 100644
--- a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -31,7 +31,7 @@ describe 'User comments on a diff', :js do
page.within('.files > div:nth-child(3)') do
expect(page).to have_content('Line is wrong')
- find('.js-toggle-diff-comments').click
+ find('.js-btn-vue-toggle-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -64,7 +64,7 @@ describe 'User comments on a diff', :js do
# Hide the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').click
+ find('.js-btn-vue-toggle-comments').click
expect(page).not_to have_content('Line is wrong')
end
@@ -77,7 +77,7 @@ describe 'User comments on a diff', :js do
# Show the comment.
page.within('.files > div:nth-child(3)') do
- find('.js-toggle-diff-comments').click
+ find('.js-btn-vue-toggle-comments').click
end
# Now both the comments should be shown.
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index df8528e79dd..bbe08ff83ff 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -25,6 +25,22 @@ describe 'New project' do
expect(page).to have_link('GitLab export')
end
+ describe 'manifest import option' do
+ before do
+ visit new_project_path
+
+ find('#import-project-tab').click
+ end
+
+ context 'when using postgres', :postgresql do
+ it { expect(page).to have_link('Manifest file') }
+ end
+
+ context 'when using mysql', :mysql do
+ it { expect(page).not_to have_link('Manifest file') }
+ end
+ end
+
context 'Visibility level selector', :js do
Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do
@@ -201,5 +217,16 @@ describe 'New project' do
expect(current_path).to eq new_import_google_code_path
end
end
+
+ context 'from manifest file', :postgresql do
+ before do
+ first('.import_manifest').click
+ end
+
+ it 'shows import instructions' do
+ expect(page).to have_content('Manifest file import')
+ expect(current_path).to eq new_import_manifest_path
+ end
+ end
end
end
diff --git a/spec/features/projects/settings/user_transfers_a_project_spec.rb b/spec/features/projects/settings/user_transfers_a_project_spec.rb
index 96b7cf1f93b..2fdbc04fa62 100644
--- a/spec/features/projects/settings/user_transfers_a_project_spec.rb
+++ b/spec/features/projects/settings/user_transfers_a_project_spec.rb
@@ -10,7 +10,7 @@ describe 'Projects > Settings > User transfers a project', :js do
sign_in(user)
end
- def transfer_project(project, group)
+ def transfer_project(project, group, confirm: true)
visit edit_project_path(project)
page.within('.js-project-transfer-form') do
@@ -21,6 +21,8 @@ describe 'Projects > Settings > User transfers a project', :js do
click_button('Transfer project')
+ return unless confirm
+
fill_in 'confirm_name_input', with: project.name
click_button 'Confirm'
@@ -28,6 +30,11 @@ describe 'Projects > Settings > User transfers a project', :js do
wait_for_requests
end
+ it 'focuses on the confirmation field' do
+ transfer_project(project, group, confirm: false)
+ expect(page).to have_selector '#confirm_name_input:focus'
+ end
+
it 'allows transferring a project to a group' do
old_path = project_path(project)
transfer_project(project, group)
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index 31b105229be..546619e88ec 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -16,4 +16,36 @@ describe 'Projects > Show > User manages notifications', :js do
expect(page).to have_content 'On mention'
end
end
+
+ context 'custom notification settings' do
+ let(:email_events) do
+ [
+ :new_note,
+ :new_issue,
+ :reopen_issue,
+ :close_issue,
+ :reassign_issue,
+ :issue_due,
+ :new_merge_request,
+ :push_to_merge_request,
+ :reopen_merge_request,
+ :close_merge_request,
+ :reassign_merge_request,
+ :merge_merge_request,
+ :failed_pipeline,
+ :success_pipeline
+ ]
+ end
+
+ it 'shows notification settings checkbox' do
+ first('.notifications-btn').click
+ page.find('a[data-notification-level="custom"]').click
+
+ page.within('.custom-notifications-form') do
+ email_events.each do |event_name|
+ expect(page).to have_selector("input[name='notification_setting[#{event_name}]']")
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 057b49cc68c..d3aa4912099 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -22,9 +22,7 @@ describe 'Multi-file editor new directory', :js do
end
it 'creates directory in current directory' do
- find('.add-to-tree').click
-
- click_link('New directory')
+ all('.ide-tree-header button').last.click
page.within('.modal') do
find('.form-control').set('folder name')
@@ -32,9 +30,7 @@ describe 'Multi-file editor new directory', :js do
click_button('Create directory')
end
- find('.add-to-tree').click
-
- click_link('New file')
+ first('.ide-tree-header button').click
page.within('.modal-dialog') do
find('.form-control').set('file name')
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index b324ab01383..f836783cbff 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -22,9 +22,7 @@ describe 'Multi-file editor new file', :js do
end
it 'creates file in current directory' do
- find('.add-to-tree').click
-
- click_link('New file')
+ first('.ide-tree-header button').click
page.within('.modal') do
find('.form-control').set('file name')
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index 28da0a87f22..dcf7d314f8e 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -24,14 +24,10 @@ describe 'Multi-file editor upload file', :js do
end
it 'uploads text file' do
- find('.add-to-tree').click
-
# make the field visible so capybara can use it
execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
attach_file('file-upload', txt_file)
- find('.add-to-tree').click
-
expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 00946bccd9a..39b47d99040 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -155,6 +155,12 @@ describe 'Project' do
visit edit_project_path(project)
end
+ it 'focuses on the confirmation field' do
+ click_button 'Remove project'
+
+ expect(page).to have_selector '#confirm_name_input:focus'
+ end
+
it 'removes a project' do
expect { remove_with_confirm('Remove project', project.path) }.to change { Project.count }.by(-1)
expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 6061021d3b0..9747b9402a7 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -5,76 +5,12 @@ describe TodosFinder do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
- let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, source_project: project) }
let(:finder) { described_class }
before do
group.add_developer(user)
end
- describe '#execute' do
- context 'visibility' do
- let(:private_group_access) { create(:group, :private) }
- let(:private_group_hidden) { create(:group, :private) }
- let(:public_project) { create(:project, :public) }
- let(:private_project_hidden) { create(:project) }
- let(:public_group) { create(:group) }
-
- let!(:todo1) { create(:todo, user: user, project: project, group: nil) }
- let!(:todo2) { create(:todo, user: user, project: public_project, group: nil) }
- let!(:todo3) { create(:todo, user: user, project: private_project_hidden, group: nil) }
- let!(:todo4) { create(:todo, user: user, project: nil, group: group) }
- let!(:todo5) { create(:todo, user: user, project: nil, group: private_group_access) }
- let!(:todo6) { create(:todo, user: user, project: nil, group: private_group_hidden) }
- let!(:todo7) { create(:todo, user: user, project: nil, group: public_group) }
-
- before do
- private_group_access.add_developer(user)
- end
-
- it 'returns only todos with a target a user has access to' do
- todos = finder.new(user).execute
-
- expect(todos).to match_array([todo1, todo2, todo4, todo5, todo7])
- end
- end
-
- context 'filtering' do
- let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
- let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
-
- it 'returns correct todos when filtered by a project' do
- todos = finder.new(user, { project_id: project.id }).execute
-
- expect(todos).to match_array([todo1])
- end
-
- it 'returns correct todos when filtered by a group' do
- todos = finder.new(user, { group_id: group.id }).execute
-
- expect(todos).to match_array([todo1, todo2])
- end
-
- it 'returns correct todos when filtered by a type' do
- todos = finder.new(user, { type: 'Issue' }).execute
-
- expect(todos).to match_array([todo1])
- end
-
- context 'with subgroups', :nested_groups do
- let(:subgroup) { create(:group, parent: group) }
- let!(:todo3) { create(:todo, user: user, group: subgroup, target: issue) }
-
- it 'returns todos from subgroups when filtered by a group' do
- todos = finder.new(user, { group_id: group.id }).execute
-
- expect(todos).to match_array([todo1, todo2, todo3])
- end
- end
- end
- end
-
describe '#sort' do
context 'by date' do
let!(:todo1) { create(:todo, user: user, project: project) }
diff --git a/spec/fixtures/aosp_manifest.xml b/spec/fixtures/aosp_manifest.xml
new file mode 100644
index 00000000000..cfd0094b735
--- /dev/null
+++ b/spec/fixtures/aosp_manifest.xml
@@ -0,0 +1,685 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+
+ <remote name="aosp"
+ fetch=".."
+ review="https://android-review.googlesource.com/" />
+ <default revision="master"
+ remote="aosp"
+ sync-j="4" />
+
+ <project path="build/make" name="platform/build" groups="pdk" >
+ <copyfile src="core/root.mk" dest="Makefile" />
+ <linkfile src="CleanSpec.mk" dest="build/CleanSpec.mk" />
+ <linkfile src="buildspec.mk.default" dest="build/buildspec.mk.default" />
+ <linkfile src="core" dest="build/core" />
+ <linkfile src="envsetup.sh" dest="build/envsetup.sh" />
+ <linkfile src="target" dest="build/target" />
+ <linkfile src="tools" dest="build/tools" />
+ </project>
+ <project path="build/blueprint" name="platform/build/blueprint" groups="pdk,tradefed" />
+ <project path="build/kati" name="platform/build/kati" groups="pdk,tradefed" />
+ <project path="build/soong" name="platform/build/soong" groups="pdk,tradefed" >
+ <linkfile src="root.bp" dest="Android.bp" />
+ <linkfile src="bootstrap.bash" dest="bootstrap.bash" />
+ </project>
+ <project path="art" name="platform/art" groups="pdk" />
+ <project path="bionic" name="platform/bionic" groups="pdk" />
+ <project path="bootable/recovery" name="platform/bootable/recovery" groups="pdk" />
+ <project path="compatibility/cdd" name="platform/compatibility/cdd" groups="pdk" />
+ <project path="cts" name="platform/cts" groups="cts,pdk-cw-fs,pdk-fs" />
+ <project path="dalvik" name="platform/dalvik" groups="pdk-cw-fs,pdk-fs" />
+ <project path="developers/build" name="platform/developers/build" groups="developers" />
+ <project path="developers/demos" name="platform/developers/demos" groups="developers" />
+ <project path="developers/samples/android" name="platform/developers/samples/android" groups="developers" />
+ <project path="development" name="platform/development" groups="developers,pdk-cw-fs,pdk-fs" />
+ <project path="device/asus/fugu" name="device/asus/fugu" groups="device,fugu,broadcom_pdk" />
+ <project path="device/asus/fugu-kernel" name="device/asus/fugu-kernel" groups="device,fugu,broadcom_pdk" clone-depth="1" />
+ <project path="device/common" name="device/common" groups="pdk-cw-fs,pdk" />
+ <project path="device/generic/arm64" name="device/generic/arm64" groups="pdk" />
+ <project path="device/generic/armv7-a-neon" name="device/generic/armv7-a-neon" groups="pdk" />
+ <project path="device/generic/car" name="device/generic/car" groups="pdk" />
+ <project path="device/generic/common" name="device/generic/common" groups="pdk" />
+ <project path="device/generic/goldfish" name="device/generic/goldfish" groups="pdk" />
+ <project path="device/generic/goldfish-opengl" name="device/generic/goldfish-opengl" groups="pdk" />
+ <project path="device/generic/mini-emulator-arm64" name="device/generic/mini-emulator-arm64" groups="pdk" />
+ <project path="device/generic/mini-emulator-armv7-a-neon" name="device/generic/mini-emulator-armv7-a-neon" groups="pdk" />
+ <project path="device/generic/mini-emulator-x86" name="device/generic/mini-emulator-x86" groups="pdk" />
+ <project path="device/generic/mini-emulator-x86_64" name="device/generic/mini-emulator-x86_64" groups="pdk" />
+ <project path="device/generic/qemu" name="device/generic/qemu" groups="pdk" />
+ <project path="device/generic/uml" name="device/generic/uml" groups="device,pdk" />
+ <project path="device/generic/x86" name="device/generic/x86" groups="pdk" />
+ <project path="device/generic/x86_64" name="device/generic/x86_64" groups="pdk" />
+ <project path="device/google/accessory/arduino" name="device/google/accessory/arduino" groups="device,pdk" />
+ <project path="device/google/accessory/demokit" name="device/google/accessory/demokit" groups="device,pdk" />
+ <project path="device/google/atv" name="device/google/atv" groups="device,broadcom_pdk,generic_fs,pdk" />
+ <project path="device/google/contexthub" name="device/google/contexthub" groups="device,marlin,pdk" />
+ <project path="device/google/cuttlefish" name="device/google/cuttlefish" groups="device" />
+ <project path="device/google/cuttlefish_common" name="device/google/cuttlefish_common" groups="device" />
+ <project path="device/google/cuttlefish_kernel" name="device/google/cuttlefish_kernel" groups="device" clone-depth="1" />
+ <project path="device/google/dragon" name="device/google/dragon" groups="device,dragon" />
+ <project path="device/google/dragon-kernel" name="device/google/dragon-kernel" groups="device,dragon" clone-depth="1" />
+ <project path="device/google/marlin" name="device/google/marlin" groups="device,marlin,pdk" />
+ <project path="device/google/marlin-kernel" name="device/google/marlin-kernel" groups="device,marlin,pdk" clone-depth="1" />
+ <project path="device/google/muskie" name="device/google/muskie" groups="device,muskie" />
+ <project path="device/google/taimen" name="device/google/taimen" groups="device,taimen" />
+ <project path="device/google/vrservices" name="device/google/vrservices" groups="pdk" clone-depth="1" />
+ <project path="device/google/wahoo" name="device/google/wahoo" groups="device,wahoo" />
+ <project path="device/google/wahoo-kernel" name="device/google/wahoo-kernel" groups="device,wahoo" clone-depth="1" />
+ <project path="device/huawei/angler" name="device/huawei/angler" groups="device,angler,broadcom_pdk" />
+ <project path="device/huawei/angler-kernel" name="device/huawei/angler-kernel" groups="device,angler,broadcom_pdk" clone-depth="1" />
+ <project path="device/lge/bullhead" name="device/lge/bullhead" groups="device,bullhead" />
+ <project path="device/lge/bullhead-kernel" name="device/lge/bullhead-kernel" groups="device,bullhead" clone-depth="1" />
+ <project path="device/linaro/bootloader/arm-trusted-firmware" name="device/linaro/bootloader/arm-trusted-firmware" />
+ <project path="device/linaro/bootloader/edk2" name="device/linaro/bootloader/edk2" />
+ <project path="device/linaro/bootloader/OpenPlatformPkg" name="device/linaro/bootloader/OpenPlatformPkg" />
+ <project path="device/linaro/hikey" name="device/linaro/hikey" groups="device,hikey,pdk" />
+ <project path="device/linaro/hikey-kernel" name="device/linaro/hikey-kernel" groups="device,hikey,pdk" clone-depth="1" />
+ <project path="device/sample" name="device/sample" groups="pdk" />
+ <project path="external/aac" name="platform/external/aac" groups="pdk" />
+ <project path="external/abi-compliance-checker" name="platform/external/abi-compliance-checker" groups="pdk" />
+ <project path="external/abi-dumper" name="platform/external/abi-dumper" groups="pdk" />
+ <project path="external/adt-infra" name="platform/external/adt-infra" groups="adt-infra,notdefault,pdk-fs" />
+ <project path="external/android-clat" name="platform/external/android-clat" groups="pdk" />
+ <project path="external/androidplot" name="platform/external/androidplot" groups="pdk" />
+ <project path="external/annotation-tools" name="platform/external/annotation-tools" groups="pdk" />
+ <project path="external/ant-glob" name="platform/external/ant-glob" groups="pdk" />
+ <project path="external/antlr" name="platform/external/antlr" groups="pdk" />
+ <project path="external/apache-commons-math" name="platform/external/apache-commons-math" groups="pdk" />
+ <project path="external/apache-harmony" name="platform/external/apache-harmony" groups="pdk" />
+ <project path="external/apache-http" name="platform/external/apache-http" groups="pdk" />
+ <project path="external/apache-xml" name="platform/external/apache-xml" groups="pdk" />
+ <project path="external/archive-patcher" name="platform/external/archive-patcher" groups="pdk" />
+ <project path="external/arm-neon-tests" name="platform/external/arm-neon-tests" groups="vendor" />
+ <project path="external/autotest" name="platform/external/autotest" groups="pdk-fs" />
+ <project path="external/avb" name="platform/external/avb" groups="pdk" />
+ <project path="external/bart" name="platform/external/bart" groups="pdk" />
+ <project path="external/blktrace" name="platform/external/blktrace" groups="pdk" />
+ <project path="external/boringssl" name="platform/external/boringssl" groups="pdk" />
+ <project path="external/bouncycastle" name="platform/external/bouncycastle" groups="pdk" />
+ <project path="external/brotli" name="platform/external/brotli" groups="pdk" />
+ <project path="external/bsdiff" name="platform/external/bsdiff" groups="pdk" />
+ <project path="external/bzip2" name="platform/external/bzip2" groups="pdk" />
+ <project path="external/caliper" name="platform/external/caliper" groups="pdk" />
+ <project path="external/cblas" name="platform/external/cblas" groups="pdk" />
+ <project path="external/chromium-libpac" name="platform/external/chromium-libpac" groups="pdk" />
+ <project path="external/chromium-trace" name="platform/external/chromium-trace" groups="pdk" />
+ <project path="external/chromium-webview" name="platform/external/chromium-webview" groups="pdk" clone-depth="1" />
+ <project path="external/clang" name="platform/external/clang" groups="pdk" />
+ <project path="external/cldr" name="platform/external/cldr" groups="pdk" />
+ <project path="external/cmockery" name="platform/external/cmockery" groups="pdk" />
+ <project path="external/cn-cbor" name="platform/external/cn-cbor" groups="pdk" />
+ <project path="external/compiler-rt" name="platform/external/compiler-rt" groups="pdk" />
+ <project path="external/conscrypt" name="platform/external/conscrypt" groups="pdk" />
+ <project path="external/crcalc" name="platform/external/crcalc" groups="pdk" />
+ <project path="external/cros/system_api" name="platform/external/cros/system_api" groups="pdk" />
+ <project path="external/curl" name="platform/external/curl" groups="pdk" />
+ <project path="external/dagger2" name="platform/external/dagger2" groups="pdk" />
+ <project path="external/deqp" name="platform/external/deqp" groups="pdk-fs" />
+ <project path="external/desugar" name="platform/external/desugar" groups="pdk" />
+ <project path="external/devlib" name="platform/external/devlib" groups="pdk" />
+ <project path="external/dexmaker" name="platform/external/dexmaker" groups="pdk" />
+ <project path="external/dhcpcd-6.8.2" name="platform/external/dhcpcd-6.8.2" groups="pdk" />
+ <project path="external/dlmalloc" name="platform/external/dlmalloc" groups="pdk" />
+ <project path="external/dng_sdk" name="platform/external/dng_sdk" groups="pdk" />
+ <project path="external/dnsmasq" name="platform/external/dnsmasq" groups="pdk" />
+ <project path="external/doclava" name="platform/external/doclava" groups="pdk" />
+ <project path="external/dokka" name="platform/external/dokka" groups="pdk" />
+ <project path="external/drm_hwcomposer" name="platform/external/drm_hwcomposer" groups="drm_hwcomposer,pdk-fs" />
+ <project path="external/droiddriver" name="platform/external/droiddriver" groups="pdk" />
+ <project path="external/drrickorang" name="platform/external/drrickorang" groups="pdk" />
+ <project path="external/dtc" name="platform/external/dtc" groups="pdk"/>
+ <project path="external/e2fsprogs" name="platform/external/e2fsprogs" groups="pdk" />
+ <project path="external/easymock" name="platform/external/easymock" groups="pdk" />
+ <project path="external/eigen" name="platform/external/eigen" groups="pdk" />
+ <project path="external/elfutils" name="platform/external/elfutils" groups="pdk" />
+ <project path="external/emma" name="platform/external/emma" groups="pdk" />
+ <project path="external/error_prone" name="platform/external/error_prone" groups="pdk" />
+ <project path="external/esd" name="platform/external/esd" groups="pdk" />
+ <project path="external/expat" name="platform/external/expat" groups="pdk" />
+ <project path="external/eyes-free" name="platform/external/eyes-free" groups="pdk" />
+ <project path="external/f2fs-tools" name="platform/external/f2fs-tools" groups="pdk" />
+ <project path="external/fdlibm" name="platform/external/fdlibm" groups="pdk" />
+ <project path="external/fec" name="platform/external/fec" groups="pdk" />
+ <project path="external/flac" name="platform/external/flac" groups="pdk" />
+ <project path="external/flatbuffers" name="platform/external/flatbuffers" groups="pdk" />
+ <project path="external/fonttools" name="platform/external/fonttools" groups="pdk" />
+ <project path="external/freetype" name="platform/external/freetype" groups="pdk" />
+ <project path="external/fsck_msdos" name="platform/external/fsck_msdos" groups="pdk" />
+ <project path="external/gemmlowp" name="platform/external/gemmlowp" groups="pdk" />
+ <project path="external/gflags" name="platform/external/gflags" groups="pdk" />
+ <project path="external/giflib" name="platform/external/giflib" groups="pdk,qcom_msm8x26" />
+ <project path="external/glide" name="platform/external/glide" groups="pdk" />
+ <project path="external/golang-protobuf" name="platform/external/golang-protobuf" groups="pdk" />
+ <project path="external/google-benchmark" name="platform/external/google-benchmark" groups="pdk" />
+ <project path="external/google-breakpad" name="platform/external/google-breakpad" groups="pdk-fs" />
+ <project path="external/google-fonts/carrois-gothic-sc" name="platform/external/google-fonts/carrois-gothic-sc" groups="pdk" />
+ <project path="external/google-fonts/coming-soon" name="platform/external/google-fonts/coming-soon" groups="pdk" />
+ <project path="external/google-fonts/cutive-mono" name="platform/external/google-fonts/cutive-mono" groups="pdk" />
+ <project path="external/google-fonts/dancing-script" name="platform/external/google-fonts/dancing-script" groups="pdk" />
+ <project path="external/google-styleguide" name="platform/external/google-styleguide" groups="pdk" />
+ <project path="external/google-tv-pairing-protocol" name="platform/external/google-tv-pairing-protocol" groups="pdk" />
+ <project path="external/googletest" name="platform/external/googletest" groups="pdk" />
+ <project path="external/gptfdisk" name="platform/external/gptfdisk" groups="pdk" />
+ <project path="external/guava" name="platform/external/guava" groups="pdk" />
+ <project path="external/guice" name="platform/external/guice" groups="pdk" />
+ <project path="external/hamcrest" name="platform/external/hamcrest" groups="pdk" />
+ <project path="external/harfbuzz_ng" name="platform/external/harfbuzz_ng" groups="pdk,qcom_msm8x26" />
+ <project path="external/hyphenation-patterns" name="platform/external/hyphenation-patterns" groups="pdk" />
+ <project path="external/icu" name="platform/external/icu" groups="pdk" />
+ <project path="external/ImageMagick" name="platform/external/ImageMagick" groups="pdk" />
+ <project path="external/ims" name="platform/external/ims" groups="pdk" />
+ <project path="external/iproute2" name="platform/external/iproute2" groups="pdk" />
+ <project path="external/ipsec-tools" name="platform/external/ipsec-tools" groups="pdk" />
+ <project path="external/iptables" name="platform/external/iptables" groups="pdk" />
+ <project path="external/iputils" name="platform/external/iputils" groups="pdk" />
+ <project path="external/iw" name="platform/external/iw" groups="pdk" />
+ <project path="external/jacoco" name="platform/external/jacoco" groups="pdk" />
+ <project path="external/jarjar" name="platform/external/jarjar" groups="pdk" />
+ <project path="external/javaparser" name="platform/external/javaparser" groups="pdk" />
+ <project path="external/javasqlite" name="platform/external/javasqlite" groups="pdk" />
+ <project path="external/javassist" name="platform/external/javassist" groups="pdk" />
+ <project path="external/jcommander" name="platform/external/jcommander" groups="pdk" />
+ <project path="external/jdiff" name="platform/external/jdiff" groups="pdk" />
+ <project path="external/jemalloc" name="platform/external/jemalloc" groups="pdk" />
+ <project path="external/jline" name="platform/external/jline" groups="pdk,tradefed,pdk-fs" />
+ <project path="external/jmdns" name="platform/external/jmdns" groups="pdk" />
+ <project path="external/jsilver" name="platform/external/jsilver" groups="pdk" />
+ <project path="external/jsmn" name="platform/external/jsmn" groups="pdk" />
+ <project path="external/jsoncpp" name="platform/external/jsoncpp" groups="pdk" />
+ <project path="external/jsr305" name="platform/external/jsr305" groups="pdk" />
+ <project path="external/jsr330" name="platform/external/jsr330" groups="pdk" />
+ <project path="external/junit" name="platform/external/junit" groups="pdk" />
+ <project path="external/junit-params" name="platform/external/junit-params" groups="pdk" />
+ <project path="external/kernel-headers" name="platform/external/kernel-headers" groups="pdk" />
+ <project path="external/kmod" name="platform/external/kmod" groups="pdk" />
+ <project path="external/kotlinc" name="platform/external/kotlinc" groups="pdk" />
+ <project path="external/ksoap2" name="platform/external/ksoap2" groups="pdk" />
+ <project path="external/libavc" name="platform/external/libavc" groups="pdk" />
+ <project path="external/libbackup" name="platform/external/libbackup" groups="pdk" />
+ <project path="external/libbrillo" name="platform/external/libbrillo" groups="pdk" />
+ <project path="external/libcap" name="platform/external/libcap" groups="pdk" />
+ <project path="external/libcap-ng" name="platform/external/libcap-ng" groups="pdk" />
+ <project path="external/libchrome" name="platform/external/libchrome" groups="pdk" />
+ <project path="external/libconstrainedcrypto" name="platform/external/libconstrainedcrypto" groups="pdk" />
+ <project path="external/libcups" name="platform/external/libcups" groups="pdk-cw-fs,pdk-fs" />
+ <project path="external/libcxx" name="platform/external/libcxx" groups="pdk" />
+ <project path="external/libcxxabi" name="platform/external/libcxxabi" groups="pdk" />
+ <project path="external/libdaemon" name="platform/external/libdaemon" groups="pdk" />
+ <project path="external/libdivsufsort" name="platform/external/libdivsufsort" groups="pdk" />
+ <project path="external/libdrm" name="platform/external/libdrm" groups="pdk" />
+ <project path="external/libedit" name="platform/external/libedit" groups="pdk" />
+ <project path="external/libese" name="platform/external/libese" groups="pdk" />
+ <project path="external/libevent" name="platform/external/libevent" groups="pdk" />
+ <project path="external/libexif" name="platform/external/libexif" groups="pdk" />
+ <project path="external/libgsm" name="platform/external/libgsm" groups="pdk" />
+ <project path="external/libhevc" name="platform/external/libhevc" groups="pdk" />
+ <project path="external/libjpeg-turbo" name="platform/external/libjpeg-turbo" groups="pdk" />
+ <project path="external/libldac" name="platform/external/libldac" groups="pdk" />
+ <project path="external/libmicrohttpd" name="platform/external/libmicrohttpd" groups="pdk" />
+ <project path="external/libmpeg2" name="platform/external/libmpeg2" groups="pdk" />
+ <project path="external/libmtp" name="platform/external/libmtp" groups="pdk" />
+ <project path="external/libnetfilter_conntrack" name="platform/external/libnetfilter_conntrack" groups="pdk" />
+ <project path="external/libnfnetlink" name="platform/external/libnfnetlink" groups="pdk" />
+ <project path="external/libnl" name="platform/external/libnl" groups="pdk" />
+ <project path="external/libogg" name="platform/external/libogg" groups="pdk" />
+ <project path="external/libopus" name="platform/external/libopus" groups="pdk" />
+ <project path="external/libpcap" name="platform/external/libpcap" groups="pdk" />
+ <project path="external/libphonenumber" name="platform/external/libphonenumber" groups="pdk" />
+ <project path="external/libpng" name="platform/external/libpng" groups="pdk" />
+ <project path="external/libtextclassifier" name="platform/external/libtextclassifier" groups="pdk" />
+ <project path="external/libunwind" name="platform/external/libunwind" groups="pdk" />
+ <project path="external/libunwind_llvm" name="platform/external/libunwind_llvm" groups="pdk" />
+ <project path="external/libusb" name="platform/external/libusb" groups="pdk" />
+ <project path="external/libusb-compat" name="platform/external/libusb-compat" groups="pdk" />
+ <project path="external/libvncserver" name="platform/external/libvncserver" groups="pdk" />
+ <project path="external/libvorbis" name="platform/external/libvorbis" groups="pdk" />
+ <project path="external/libvpx" name="platform/external/libvpx" groups="pdk" />
+ <project path="external/libvterm" name="platform/external/libvterm" groups="pdk" />
+ <project path="external/libxcam" name="platform/external/libxcam" groups="pdk" />
+ <project path="external/libxml2" name="platform/external/libxml2" groups="pdk,libxml2" />
+ <project path="external/libyuv" name="platform/external/libyuv" groups="pdk,libyuv" />
+ <project path="external/linux-kselftest" name="platform/external/linux-kselftest" groups="vts,pdk" />
+ <project path="external/lisa" name="platform/external/lisa" groups="pdk" />
+ <project path="external/llvm" name="platform/external/llvm" groups="pdk" />
+ <project path="external/lmfit" name="platform/external/lmfit" groups="pdk" />
+ <project path="external/ltp" name="platform/external/ltp" groups="vts,pdk" />
+ <project path="external/lz4" name="platform/external/lz4" groups="pdk" />
+ <project path="external/lzma" name="platform/external/lzma" groups="pdk" />
+ <project path="external/markdown" name="platform/external/markdown" groups="pdk" />
+ <project path="external/mdnsresponder" name="platform/external/mdnsresponder" groups="pdk" />
+ <project path="external/mesa3d" name="platform/external/mesa3d" groups="pdk-cw-fs,pdk-fs" />
+ <project path="external/Microsoft-GSL" name="platform/external/Microsoft-GSL" groups="pdk" />
+ <project path="external/minijail" name="platform/external/minijail" groups="pdk" />
+ <project path="external/mksh" name="platform/external/mksh" groups="pdk" />
+ <project path="external/mmc-utils" name="platform/external/mmc-utils" groups="pdk" />
+ <project path="external/mockftpserver" name="platform/external/mockftpserver" groups="pdk" />
+ <project path="external/mockito" name="platform/external/mockito" groups="pdk" />
+ <project path="external/mockwebserver" name="platform/external/mockwebserver" groups="pdk" />
+ <project path="external/modp_b64" name="platform/external/modp_b64" groups="pdk" />
+ <project path="external/mp4parser" name="platform/external/mp4parser" groups="pdk" />
+ <project path="external/mtpd" name="platform/external/mtpd" groups="pdk" />
+ <project path="external/nanohttpd" name="platform/external/nanohttpd" groups="pdk" />
+ <project path="external/nanopb-c" name="platform/external/nanopb-c" groups="pdk" />
+ <project path="external/naver-fonts" name="platform/external/naver-fonts" groups="pdk" />
+ <project path="external/neven" name="platform/external/neven" groups="pdk" />
+ <project path="external/nfacct" name="platform/external/nfacct" groups="pdk" />
+ <project path="external/nist-pkits" name="platform/external/nist-pkits" groups="pdk" />
+ <project path="external/nist-sip" name="platform/external/nist-sip" groups="pdk" />
+ <project path="external/noto-fonts" name="platform/external/noto-fonts" groups="pdk" />
+ <project path="external/oauth" name="platform/external/oauth" groups="pdk" />
+ <project path="external/objenesis" name="platform/external/objenesis" groups="pdk" />
+ <project path="external/oj-libjdwp" name="platform/external/oj-libjdwp" groups="pdk" />
+ <project path="external/okhttp" name="platform/external/okhttp" groups="pdk" />
+ <project path="external/one-true-awk" name="platform/external/one-true-awk" groups="pdk" />
+ <project path="external/opencv" name="platform/external/opencv" groups="pdk-cw-fs,pdk-fs" />
+ <project path="external/owasp/sanitizer" name="platform/external/owasp/sanitizer" groups="pdk" />
+ <project path="external/parameter-framework" name="platform/external/parameter-framework" groups="pdk" />
+ <project path="external/pcre" name="platform/external/pcre" groups="pdk" />
+ <project path="external/pdfium" name="platform/external/pdfium" groups="pdk" />
+ <project path="external/perf_data_converter" name="platform/external/perf_data_converter" groups="pdk" />
+ <project path="external/perfetto" name="platform/external/perfetto" groups="pdk" />
+ <project path="external/piex" name="platform/external/piex" groups="pdk" />
+ <project path="external/ply" name="platform/external/ply" groups="pdk" />
+ <project path="external/ppp" name="platform/external/ppp" groups="pdk" />
+ <project path="external/proguard" name="platform/external/proguard" groups="pdk" />
+ <project path="external/protobuf" name="platform/external/protobuf" groups="pdk" />
+ <project path="external/puffin" name="platform/external/puffin" groups="pdk" />
+ <project path="external/python/appdirs" name="platform/external/python/appdirs" groups="vts,pdk" />
+ <project path="external/python/cachetools" name="platform/external/python/cachetools" groups="vts,pdk" />
+ <project path="external/python/cpython2" name="platform/external/python/cpython2" groups="pdk" />
+ <project path="external/python/cpython3" name="platform/external/python/cpython3" groups="pdk" />
+ <project path="external/python/dateutil" name="platform/external/python/dateutil" groups="pdk" />
+ <project path="external/python/dill" name="platform/external/python/dill" groups="vts,pdk" />
+ <project path="external/python/enum" name="platform/external/python/enum" groups="vts,pdk" />
+ <project path="external/python/enum34" name="platform/external/python/enum34" groups="vts,pdk" />
+ <project path="external/python/future" name="platform/external/python/future" groups="vts,pdk" />
+ <project path="external/python/futures" name="platform/external/python/futures" groups="vts,pdk" />
+ <project path="external/python/gapic-google-cloud-pubsub-v1" name="platform/external/python/gapic-google-cloud-pubsub-v1" groups="vts,pdk" />
+ <project path="external/python/google-api-python-client" name="platform/external/python/google-api-python-client" groups="vts,pdk" />
+ <project path="external/python/google-auth" name="platform/external/python/google-auth" groups="vts,pdk" />
+ <project path="external/python/google-auth-httplib2" name="platform/external/python/google-auth-httplib2" groups="vts,pdk" />
+ <project path="external/python/google-cloud-core" name="platform/external/python/google-cloud-core" groups="vts,pdk" />
+ <project path="external/python/google-cloud-pubsub" name="platform/external/python/google-cloud-pubsub" groups="vts,pdk" />
+ <project path="external/python/google-gax" name="platform/external/python/google-gax" groups="vts,pdk" />
+ <project path="external/python/googleapis" name="platform/external/python/googleapis" groups="vts,pdk" />
+ <project path="external/python/grpc-google-iam-v1" name="platform/external/python/grpc-google-iam-v1" groups="vts,pdk" />
+ <project path="external/python/grpcio" name="platform/external/python/grpcio" groups="vts,pdk" />
+ <project path="external/python/httplib2" name="platform/external/python/httplib2" groups="vts,pdk" />
+ <project path="external/python/matplotlib" name="platform/external/python/matplotlib" groups="vts,pdk" />
+ <project path="external/python/numpy" name="platform/external/python/numpy" groups="vts,pdk" />
+ <project path="external/python/oauth2client" name="platform/external/python/oauth2client" groups="vts,pdk" />
+ <project path="external/python/olefile" name="platform/external/python/olefile" groups="vts,pdk" />
+ <project path="external/python/packaging" name="platform/external/python/packaging" groups="vts,pdk" />
+ <project path="external/python/parse" name="platform/external/python/parse" groups="vts,pdk" />
+ <project path="external/python/Pillow" name="platform/external/python/Pillow" groups="vts,pdk" />
+ <project path="external/python/ply" name="platform/external/python/ply" groups="vts,pdk" />
+ <project path="external/python/proto-google-cloud-pubsub-v1" name="platform/external/python/proto-google-cloud-pubsub-v1" groups="vts,pdk" />
+ <project path="external/python/protobuf" name="platform/external/python/protobuf" groups="vts,pdk" />
+ <project path="external/python/pyasn1" name="platform/external/python/pyasn1" groups="vts,pdk" />
+ <project path="external/python/pyasn1-modules" name="platform/external/python/pyasn1-modules" groups="vts,pdk" />
+ <project path="external/python/pyparsing" name="platform/external/python/pyparsing" groups="vts,pdk" />
+ <project path="external/python/requests" name="platform/external/python/requests" groups="vts,pdk" />
+ <project path="external/python/rsa" name="platform/external/python/rsa" groups="vts,pdk" />
+ <project path="external/python/scipy" name="platform/external/python/scipy" groups="vts,pdk" />
+ <project path="external/python/setuptools" name="platform/external/python/setuptools" groups="vts,pdk" />
+ <project path="external/python/six" name="platform/external/python/six" groups="vts,pdk" />
+ <project path="external/python/uritemplates" name="platform/external/python/uritemplates" groups="vts,pdk" />
+ <project path="external/replicaisland" name="platform/external/replicaisland" groups="pdk" />
+ <project path="external/rmi4utils" name="platform/external/rmi4utils" groups="pdk" />
+ <project path="external/robolectric" name="platform/external/robolectric" groups="pdk-cw-fs,pdk-fs" />
+ <project path="external/roboto-fonts" name="platform/external/roboto-fonts" groups="pdk" />
+ <project path="external/rootdev" name="platform/external/rootdev" groups="pdk" />
+ <project path="external/safe-iop" name="platform/external/safe-iop" groups="pdk" />
+ <project path="external/scapy" name="platform/external/scapy" groups="pdk-fs" />
+ <project path="external/scrypt" name="platform/external/scrypt" groups="pdk" />
+ <project path="external/seccomp-tests" name="platform/external/seccomp-tests" groups="pdk" />
+ <project path="external/selinux" name="platform/external/selinux" groups="pdk" />
+ <project path="external/sfntly" name="platform/external/sfntly" groups="pdk,qcom_msm8x26" />
+ <project path="external/shaderc/spirv-headers" name="platform/external/shaderc/spirv-headers" groups="pdk" />
+ <project path="external/shflags" name="platform/external/shflags" groups="pdk" />
+ <project path="external/skia" name="platform/external/skia" groups="pdk,qcom_msm8x26" />
+ <project path="external/sl4a" name="platform/external/sl4a" groups="pdk" />
+ <project path="external/slf4j" name="platform/external/slf4j" groups="pdk" />
+ <project path="external/smali" name="platform/external/smali" groups="pdk" />
+ <project path="external/snakeyaml" name="platform/external/snakeyaml" groups="pdk" />
+ <project path="external/sonic" name="platform/external/sonic" groups="pdk" />
+ <project path="external/sonivox" name="platform/external/sonivox" groups="pdk" />
+ <project path="external/speex" name="platform/external/speex" groups="pdk" />
+ <project path="external/spirv-llvm" name="platform/external/spirv-llvm" groups="pdk" />
+ <project path="external/sqlite" name="platform/external/sqlite" groups="pdk" />
+ <project path="external/squashfs-tools" name="platform/external/squashfs-tools" groups="pdk" />
+ <project path="external/strace" name="platform/external/strace" groups="pdk" />
+ <project path="external/stressapptest" name="platform/external/stressapptest" groups="pdk" />
+ <project path="external/subsampling-scale-image-view" name="platform/external/subsampling-scale-image-view" clone-depth="1" />
+ <project path="external/swiftshader" name="platform/external/swiftshader" groups="pdk" />
+ <project path="external/syslinux" name="platform/external/syslinux" groups="pdk" />
+ <project path="external/tagsoup" name="platform/external/tagsoup" groups="pdk" />
+ <project path="external/tcpdump" name="platform/external/tcpdump" groups="pdk" />
+ <project path="external/tensorflow" name="platform/external/tensorflow" groups="pdk" />
+ <project path="external/testng" name="platform/external/testng" groups="pdk" />
+ <project path="external/tinyalsa" name="platform/external/tinyalsa" groups="pdk" />
+ <project path="external/tinycompress" name="platform/external/tinycompress" groups="pdk" />
+ <project path="external/tinyxml" name="platform/external/tinyxml" groups="pdk" />
+ <project path="external/tinyxml2" name="platform/external/tinyxml2" groups="pdk" />
+ <project path="external/toolchain-utils" name="platform/external/toolchain-utils" />
+ <project path="external/toybox" name="platform/external/toybox" groups="pdk" />
+ <project path="external/tpm2" name="platform/external/tpm2" groups="pdk" />
+ <project path="external/trappy" name="platform/external/trappy" groups="pdk" />
+ <project path="external/tremolo" name="platform/external/tremolo" groups="pdk" />
+ <project path="external/turbine" name="platform/external/turbine" groups="pdk" />
+ <project path="external/unicode" name="platform/external/unicode" groups="pdk" />
+ <project path="external/universal-tween-engine" name="platform/external/universal-tween-engine" />
+ <project path="external/v4l2_codec2" name="platform/external/v4l2_codec2" groups="pdk" />
+ <project path="external/v8" name="platform/external/v8" groups="pdk" />
+ <project path="external/valgrind" name="platform/external/valgrind" groups="pdk" />
+ <project path="external/vboot_reference" name="platform/external/vboot_reference" groups="vboot,pdk-fs" />
+ <project path="external/vixl" name="platform/external/vixl" groups="pdk" />
+ <project path="external/vogar" name="platform/external/vogar" groups="pdk" />
+ <project path="external/volley" name="platform/external/volley" groups="pdk" />
+ <project path="external/vulkan-validation-layers" name="platform/external/vulkan-validation-layers" groups="pdk" />
+ <project path="external/walt" name="platform/external/walt" groups="pdk" />
+ <project path="external/webp" name="platform/external/webp" groups="pdk,qcom_msm8x26" />
+ <project path="external/webrtc" name="platform/external/webrtc" groups="pdk" />
+ <project path="external/webview_support_interfaces" name="platform/external/webview_support_interfaces" groups="pdk" />
+ <project path="external/wpa_supplicant_8" name="platform/external/wpa_supplicant_8" groups="pdk" />
+ <project path="external/wycheproof" name="platform/external/wycheproof" groups="pdk" />
+ <project path="external/x264" name="platform/external/x264" groups="pdk" />
+ <project path="external/xmlrpcpp" name="platform/external/xmlrpcpp" groups="pdk" />
+ <project path="external/xmp_toolkit" name="platform/external/xmp_toolkit" groups="pdk" />
+ <project path="external/xz-embedded" name="platform/external/xz-embedded" groups="pdk" />
+ <project path="external/zlib" name="platform/external/zlib" groups="pdk" />
+ <project path="external/zopfli" name="platform/external/zopfli" groups="pdk" />
+ <project path="external/zxing" name="platform/external/zxing" groups="pdk" />
+ <project path="frameworks/av" name="platform/frameworks/av" groups="pdk" />
+ <project path="frameworks/base" name="platform/frameworks/base" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/compile/libbcc" name="platform/frameworks/compile/libbcc" groups="pdk" />
+ <project path="frameworks/compile/mclinker" name="platform/frameworks/compile/mclinker" groups="pdk" />
+ <project path="frameworks/compile/slang" name="platform/frameworks/compile/slang" groups="pdk" />
+ <project path="frameworks/data-binding" name="platform/frameworks/data-binding" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/ex" name="platform/frameworks/ex" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/hardware/interfaces" name="platform/frameworks/hardware/interfaces" groups="pdk" />
+ <project path="frameworks/layoutlib" name="platform/frameworks/layoutlib" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/minikin" name="platform/frameworks/minikin" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/ml" name="platform/frameworks/ml" groups="pdk" />
+ <project path="frameworks/multidex" name="platform/frameworks/multidex" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/native" name="platform/frameworks/native" groups="pdk" />
+ <project path="frameworks/opt/bitmap" name="platform/frameworks/opt/bitmap" groups="pdk-fs" />
+ <project path="frameworks/opt/calendar" name="platform/frameworks/opt/calendar" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/car/services" name="platform/frameworks/opt/car/services" groups="pdk-fs" />
+ <project path="frameworks/opt/chips" name="platform/frameworks/opt/chips" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/colorpicker" name="platform/frameworks/opt/colorpicker" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/datetimepicker" name="platform/frameworks/opt/datetimepicker" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/inputmethodcommon" name="platform/frameworks/opt/inputmethodcommon" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/net/ethernet" name="platform/frameworks/opt/net/ethernet" groups="pdk-fs" />
+ <project path="frameworks/opt/net/ims" name="platform/frameworks/opt/net/ims" groups="frameworks_ims,pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/net/lowpan" name="platform/frameworks/opt/net/lowpan" groups="pdk-fs" />
+ <project path="frameworks/opt/net/voip" name="platform/frameworks/opt/net/voip" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/net/wifi" name="platform/frameworks/opt/net/wifi" groups="pdk" />
+ <project path="frameworks/opt/photoviewer" name="platform/frameworks/opt/photoviewer" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/setupwizard" name="platform/frameworks/opt/setupwizard" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/telephony" name="platform/frameworks/opt/telephony" groups="pdk" />
+ <project path="frameworks/opt/timezonepicker" name="platform/frameworks/opt/timezonepicker" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/opt/vcard" name="platform/frameworks/opt/vcard" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/rs" name="platform/frameworks/rs" groups="pdk" />
+ <project path="frameworks/support" name="platform/frameworks/support" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/webview" name="platform/frameworks/webview" groups="pdk-cw-fs,pdk-fs" />
+ <project path="frameworks/wilhelm" name="platform/frameworks/wilhelm" groups="pdk-cw-fs,pdk-fs" />
+ <project path="hardware/akm" name="platform/hardware/akm" groups="pdk" />
+ <project path="hardware/broadcom/libbt" name="platform/hardware/broadcom/libbt" groups="pdk" />
+ <project path="hardware/broadcom/wlan" name="platform/hardware/broadcom/wlan" groups="pdk,broadcom_wlan" />
+ <project path="hardware/google/apf" name="platform/hardware/google/apf" groups="pdk" />
+ <project path="hardware/google/easel" name="platform/hardware/google/easel" groups="pdk,easel" />
+ <project path="hardware/google/interfaces" name="platform/hardware/google/interfaces" groups="pdk" />
+ <project path="hardware/intel/audio_media" name="platform/hardware/intel/audio_media" groups="intel,pdk" />
+ <project path="hardware/intel/bootstub" name="platform/hardware/intel/bootstub" groups="intel,pdk" />
+ <project path="hardware/intel/common/libmix" name="platform/hardware/intel/common/libmix" groups="intel,pdk" />
+ <project path="hardware/intel/common/libstagefrighthw" name="platform/hardware/intel/common/libstagefrighthw" groups="intel,pdk" />
+ <project path="hardware/intel/common/libva" name="platform/hardware/intel/common/libva" groups="intel,pdk" />
+ <project path="hardware/intel/common/libwsbm" name="platform/hardware/intel/common/libwsbm" groups="intel,pdk" />
+ <project path="hardware/intel/common/omx-components" name="platform/hardware/intel/common/omx-components" groups="intel,pdk" />
+ <project path="hardware/intel/common/utils" name="platform/hardware/intel/common/utils" groups="intel,pdk" />
+ <project path="hardware/intel/common/wrs_omxil_core" name="platform/hardware/intel/common/wrs_omxil_core" groups="intel,pdk" />
+ <project path="hardware/intel/img/hwcomposer" name="platform/hardware/intel/img/hwcomposer" groups="intel,pdk" />
+ <project path="hardware/intel/img/psb_headers" name="platform/hardware/intel/img/psb_headers" groups="intel,pdk" />
+ <project path="hardware/intel/img/psb_video" name="platform/hardware/intel/img/psb_video" groups="intel,pdk" />
+ <project path="hardware/interfaces" name="platform/hardware/interfaces" groups="pdk" />
+ <project path="hardware/invensense" name="platform/hardware/invensense" groups="invensense,pdk" />
+ <project path="hardware/libhardware" name="platform/hardware/libhardware" groups="pdk" />
+ <project path="hardware/libhardware_legacy" name="platform/hardware/libhardware_legacy" groups="pdk" />
+ <project path="hardware/marvell/bt" name="platform/hardware/marvell/bt" groups="marvell_bt,pdk" />
+ <project path="hardware/nxp/nfc" name="platform/hardware/nxp/nfc" groups="pdk" />
+ <project path="hardware/nxp/secure_element" name="platform/hardware/nxp/secure_element" groups="pdk" />
+ <project path="hardware/qcom/audio" name="platform/hardware/qcom/audio" groups="qcom,qcom_audio,pdk" />
+ <project path="hardware/qcom/bootctrl" name="platform/hardware/qcom/bootctrl" groups="pdk" />
+ <project path="hardware/qcom/bt" name="platform/hardware/qcom/bt" groups="qcom,pdk" />
+ <project path="hardware/qcom/camera" name="platform/hardware/qcom/camera" groups="qcom_camera,pdk" />
+ <project path="hardware/qcom/data/ipacfg-mgr" name="platform/hardware/qcom/data/ipacfg-mgr" groups="qcom,pdk" />
+ <project path="hardware/qcom/display" name="platform/hardware/qcom/display" groups="pdk,qcom,qcom_display" />
+ <project path="hardware/qcom/gps" name="platform/hardware/qcom/gps" groups="qcom,qcom_gps,pdk" />
+ <project path="hardware/qcom/keymaster" name="platform/hardware/qcom/keymaster" groups="qcom,qcom_keymaster,pdk" />
+ <project path="hardware/qcom/media" name="platform/hardware/qcom/media" groups="qcom,pdk" />
+ <project path="hardware/qcom/msm8960" name="platform/hardware/qcom/msm8960" groups="qcom_msm8960,pdk" />
+ <project path="hardware/qcom/msm8994" name="platform/hardware/qcom/msm8994" groups="qcom_msm8994,pdk" />
+ <project path="hardware/qcom/msm8996" name="platform/hardware/qcom/msm8996" groups="qcom_msm8996,pdk" />
+ <project path="hardware/qcom/msm8998" name="platform/hardware/qcom/msm8998" groups="qcom_msm8998,pdk" />
+ <project path="hardware/qcom/msm8x09" name="platform/hardware/qcom/msm8x09" groups="qcom_msm8x09" />
+ <project path="hardware/qcom/msm8x26" name="platform/hardware/qcom/msm8x26" groups="qcom_msm8x26,pdk" />
+ <project path="hardware/qcom/msm8x27" name="platform/hardware/qcom/msm8x27" groups="qcom_msm8x27,pdk" />
+ <project path="hardware/qcom/msm8x84" name="platform/hardware/qcom/msm8x84" groups="qcom_msm8x84,pdk" />
+ <project path="hardware/qcom/neuralnetworks/hvxservice" name="platform/hardware/qcom/neuralnetworks/hvxservice" groups="wahoo" />
+ <project path="hardware/qcom/power" name="platform/hardware/qcom/power" groups="qcom,pdk" />
+ <project path="hardware/qcom/wlan" name="platform/hardware/qcom/wlan" groups="qcom_wlan,pdk" />
+ <project path="hardware/ril" name="platform/hardware/ril" groups="pdk" />
+ <project path="hardware/st/nfc" name="platform/hardware/st/nfc" groups="pdk" />
+ <project path="kernel/configs" name="kernel/configs" groups="vts,pdk" />
+ <project path="kernel/tests" name="kernel/tests" />
+ <project path="libcore" name="platform/libcore" groups="pdk" />
+ <project path="libnativehelper" name="platform/libnativehelper" groups="pdk" />
+ <project path="packages/apps/BasicSmsReceiver" name="platform/packages/apps/BasicSmsReceiver" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/Bluetooth" name="platform/packages/apps/Bluetooth" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/Browser2" name="platform/packages/apps/Browser2" groups="pdk-fs" />
+ <project path="packages/apps/Calendar" name="platform/packages/apps/Calendar" groups="pdk-fs" />
+ <project path="packages/apps/Camera2" name="platform/packages/apps/Camera2" groups="pdk-fs" />
+ <project path="packages/apps/Car/Dialer" name="platform/packages/apps/Car/Dialer" groups="pdk-fs" />
+ <project path="packages/apps/Car/Hvac" name="platform/packages/apps/Car/Hvac" groups="pdk-fs" />
+ <project path="packages/apps/Car/LatinIME" name="platform/packages/apps/Car/LatinIME" groups="pdk-fs" />
+ <project path="packages/apps/Car/Launcher" name="platform/packages/apps/Car/Launcher" groups="pdk-fs" />
+ <project path="packages/apps/Car/LensPicker" name="platform/packages/apps/Car/LensPicker" groups="pdk-fs" />
+ <project path="packages/apps/Car/libs" name="platform/packages/apps/Car/libs" groups="pdk-fs" />
+ <project path="packages/apps/Car/LocalMediaPlayer" name="platform/packages/apps/Car/LocalMediaPlayer" groups="pdk-fs" />
+ <project path="packages/apps/Car/Media" name="platform/packages/apps/Car/Media" groups="pdk-fs" />
+ <project path="packages/apps/Car/Messenger" name="platform/packages/apps/Car/Messenger" groups="pdk-fs" />
+ <project path="packages/apps/Car/Overview" name="platform/packages/apps/Car/Overview" groups="pdk-fs" />
+ <project path="packages/apps/Car/Radio" name="platform/packages/apps/Car/Radio" groups="pdk-fs" />
+ <project path="packages/apps/Car/Settings" name="platform/packages/apps/Car/Settings" groups="pdk-fs" />
+ <project path="packages/apps/Car/Stream" name="platform/packages/apps/Car/Stream" groups="pdk-fs" />
+ <project path="packages/apps/Car/SystemUpdater" name="platform/packages/apps/Car/SystemUpdater" groups="pdk-fs" />
+ <project path="packages/apps/CarrierConfig" name="platform/packages/apps/CarrierConfig" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/CellBroadcastReceiver" name="platform/packages/apps/CellBroadcastReceiver" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/CertInstaller" name="platform/packages/apps/CertInstaller" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/Contacts" name="platform/packages/apps/Contacts" groups="pdk-fs" />
+ <project path="packages/apps/DeskClock" name="platform/packages/apps/DeskClock" groups="pdk-fs" />
+ <project path="packages/apps/DevCamera" name="platform/packages/apps/DevCamera" groups="pdk" />
+ <project path="packages/apps/Dialer" name="platform/packages/apps/Dialer" groups="pdk-fs" />
+ <project path="packages/apps/DocumentsUI" name="platform/packages/apps/DocumentsUI" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/Email" name="platform/packages/apps/Email" groups="pdk-fs" />
+ <project path="packages/apps/EmergencyInfo" name="platform/packages/apps/EmergencyInfo" groups="pdk-fs" />
+ <project path="packages/apps/ExactCalculator" name="platform/packages/apps/ExactCalculator" groups="pdk-fs" />
+ <project path="packages/apps/Gallery" name="platform/packages/apps/Gallery" groups="pdk-fs" />
+ <project path="packages/apps/Gallery2" name="platform/packages/apps/Gallery2" groups="pdk-fs" />
+ <project path="packages/apps/HTMLViewer" name="platform/packages/apps/HTMLViewer" groups="pdk-fs" />
+ <project path="packages/apps/KeyChain" name="platform/packages/apps/KeyChain" groups="pdk-fs" />
+ <project path="packages/apps/Launcher2" name="platform/packages/apps/Launcher2" groups="pdk-fs" />
+ <project path="packages/apps/Launcher3" name="platform/packages/apps/Launcher3" groups="pdk-fs" />
+ <project path="packages/apps/LegacyCamera" name="platform/packages/apps/LegacyCamera" groups="pdk-fs" />
+ <project path="packages/apps/ManagedProvisioning" name="platform/packages/apps/ManagedProvisioning" groups="pdk-fs" />
+ <project path="packages/apps/Messaging" name="platform/packages/apps/Messaging" groups="pdk-fs" />
+ <project path="packages/apps/Music" name="platform/packages/apps/Music" groups="pdk-fs" />
+ <project path="packages/apps/MusicFX" name="platform/packages/apps/MusicFX" groups="pdk-fs" />
+ <project path="packages/apps/Nfc" name="platform/packages/apps/Nfc" groups="apps_nfc,pdk-fs" />
+ <project path="packages/apps/OneTimeInitializer" name="platform/packages/apps/OneTimeInitializer" groups="pdk-fs" />
+ <project path="packages/apps/PackageInstaller" name="platform/packages/apps/PackageInstaller" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/PhoneCommon" name="platform/packages/apps/PhoneCommon" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/apps/Protips" name="platform/packages/apps/Protips" groups="pdk-fs" />
+ <project path="packages/apps/Provision" name="platform/packages/apps/Provision" groups="pdk-fs" />
+ <project path="packages/apps/QuickSearchBox" name="platform/packages/apps/QuickSearchBox" groups="pdk-fs" />
+ <project path="packages/apps/SafetyRegulatoryInfo" name="platform/packages/apps/SafetyRegulatoryInfo" groups="pdk-fs" />
+ <project path="packages/apps/SecureElement" name="platform/packages/apps/SecureElement" groups="apps_se,pdk-fs" />
+ <project path="packages/apps/Settings" name="platform/packages/apps/Settings" groups="pdk-fs" />
+ <project path="packages/apps/SoundRecorder" name="platform/packages/apps/SoundRecorder" groups="pdk-fs" />
+ <project path="packages/apps/SpareParts" name="platform/packages/apps/SpareParts" groups="pdk-fs" />
+ <project path="packages/apps/Stk" name="platform/packages/apps/Stk" groups="apps_stk,pdk-fs" />
+ <project path="packages/apps/StorageManager" name="platform/packages/apps/StorageManager" groups="pdk-fs" />
+ <project path="packages/apps/Tag" name="platform/packages/apps/Tag" groups="pdk-fs" />
+ <project path="packages/apps/Terminal" name="platform/packages/apps/Terminal" groups="pdk-fs" />
+ <project path="packages/apps/Test/connectivity" name="platform/packages/apps/Test/connectivity" groups="pdk" />
+ <project path="packages/apps/TimeZoneData" name="platform/packages/apps/TimeZoneData" groups="pdk" />
+ <project path="packages/apps/TimeZoneUpdater" name="platform/packages/apps/TimeZoneUpdater" groups="pdk" />
+ <project path="packages/apps/Traceur" name="platform/packages/apps/Traceur" groups="pdk-fs" />
+ <project path="packages/apps/TvSettings" name="platform/packages/apps/TvSettings" groups="pdk-fs" />
+ <project path="packages/apps/TV" name="platform/packages/apps/TV" />
+ <project path="packages/apps/UnifiedEmail" name="platform/packages/apps/UnifiedEmail" groups="pdk-fs" />
+ <project path="packages/apps/WallpaperPicker" name="platform/packages/apps/WallpaperPicker" groups="pdk-fs" />
+ <project path="packages/experimental" name="platform/packages/experimental" />
+ <project path="packages/inputmethods/LatinIME" name="platform/packages/inputmethods/LatinIME" groups="pdk-fs" />
+ <project path="packages/inputmethods/OpenWnn" name="platform/packages/inputmethods/OpenWnn" groups="pdk-fs" />
+ <project path="packages/providers/ApplicationsProvider" name="platform/packages/providers/ApplicationsProvider" groups="pdk-fs" />
+ <project path="packages/providers/BlockedNumberProvider" name="platform/packages/providers/BlockedNumberProvider" groups="pdk-fs" />
+ <project path="packages/providers/BookmarkProvider" name="platform/packages/providers/BookmarkProvider" groups="pdk-fs" />
+ <project path="packages/providers/CalendarProvider" name="platform/packages/providers/CalendarProvider" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/providers/CallLogProvider" name="platform/packages/providers/CallLogProvider" groups="pdk-fs" />
+ <project path="packages/providers/ContactsProvider" name="platform/packages/providers/ContactsProvider" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/providers/DownloadProvider" name="platform/packages/providers/DownloadProvider" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/providers/MediaProvider" name="platform/packages/providers/MediaProvider" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/providers/PartnerBookmarksProvider" name="platform/packages/providers/PartnerBookmarksProvider" groups="pdk-fs" />
+ <project path="packages/providers/TelephonyProvider" name="platform/packages/providers/TelephonyProvider" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/providers/TvProvider" name="platform/packages/providers/TvProvider" groups="pdk-fs" />
+ <project path="packages/providers/UserDictionaryProvider" name="platform/packages/providers/UserDictionaryProvider" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/screensavers/Basic" name="platform/packages/screensavers/Basic" groups="pdk-fs" />
+ <project path="packages/screensavers/PhotoTable" name="platform/packages/screensavers/PhotoTable" groups="pdk-fs" />
+ <project path="packages/screensavers/WebView" name="platform/packages/screensavers/WebView" groups="pdk-fs" />
+ <project path="packages/services/BuiltInPrintService" name="platform/packages/services/BuiltInPrintService" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/services/Car" name="platform/packages/services/Car" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/services/Mms" name="platform/packages/services/Mms" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/services/NetworkRecommendation" name="platform/packages/services/NetworkRecommendation" groups="pdk-fs" />
+ <project path="packages/services/Telecomm" name="platform/packages/services/Telecomm" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/services/Telephony" name="platform/packages/services/Telephony" groups="pdk-cw-fs,pdk-fs" />
+ <project path="packages/wallpapers/LivePicker" name="platform/packages/wallpapers/LivePicker" groups="pdk-fs" />
+ <project path="pdk" name="platform/pdk" groups="pdk" />
+ <project path="platform_testing" name="platform/platform_testing" groups="pdk-fs,pdk-cw-fs,cts" />
+ <project path="prebuilts/abi-dumps/ndk" name="platform/prebuilts/abi-dumps/ndk" groups="pdk-fs" clone-depth="1" />
+ <project path="prebuilts/abi-dumps/vndk" name="platform/prebuilts/abi-dumps/vndk" groups="pdk-fs" clone-depth="1" />
+ <project path="prebuilts/android-emulator" name="platform/prebuilts/android-emulator" groups="pdk-fs" clone-depth="1" />
+ <project path="prebuilts/build-tools" name="platform/prebuilts/build-tools" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/checkstyle" name="platform/prebuilts/checkstyle" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/clang-tools" name="platform/prebuilts/clang-tools" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/clang/host/darwin-x86" name="platform/prebuilts/clang/host/darwin-x86" groups="pdk,darwin" clone-depth="1" />
+ <project path="prebuilts/clang/host/linux-x86" name="platform/prebuilts/clang/host/linux-x86" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/deqp" name="platform/prebuilts/deqp" groups="pdk-fs" clone-depth="1" />
+ <project path="prebuilts/devtools" name="platform/prebuilts/devtools" groups="pdk-fs" clone-depth="1" />
+ <project path="prebuilts/gcc/darwin-x86/aarch64/aarch64-linux-android-4.9" name="platform/prebuilts/gcc/darwin-x86/aarch64/aarch64-linux-android-4.9" groups="pdk,darwin,arm" clone-depth="1" />
+ <project path="prebuilts/gcc/darwin-x86/arm/arm-linux-androideabi-4.9" name="platform/prebuilts/gcc/darwin-x86/arm/arm-linux-androideabi-4.9" groups="pdk,darwin,arm" clone-depth="1" />
+ <project path="prebuilts/gcc/darwin-x86/host/i686-apple-darwin-4.2.1" name="platform/prebuilts/gcc/darwin-x86/host/i686-apple-darwin-4.2.1" groups="pdk,darwin" clone-depth="1" />
+ <project path="prebuilts/gcc/darwin-x86/mips/mips64el-linux-android-4.9" name="platform/prebuilts/gcc/darwin-x86/mips/mips64el-linux-android-4.9" groups="pdk,darwin,mips" clone-depth="1" />
+ <project path="prebuilts/gcc/darwin-x86/x86/x86_64-linux-android-4.9" name="platform/prebuilts/gcc/darwin-x86/x86/x86_64-linux-android-4.9" groups="pdk,darwin,x86" clone-depth="1" />
+ <project path="prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9" name="platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9" groups="pdk,linux,arm" clone-depth="1" />
+ <project path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9" groups="pdk,linux,arm" clone-depth="1" />
+ <project path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.15-4.8" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.15-4.8" groups="pdk,linux" clone-depth="1" />
+ <project path="prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8" name="platform/prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8" groups="pdk-fs" clone-depth="1" />
+ <project path="prebuilts/gcc/linux-x86/mips/mips64el-linux-android-4.9" name="platform/prebuilts/gcc/linux-x86/mips/mips64el-linux-android-4.9" groups="pdk,linux,mips" clone-depth="1" />
+ <project path="prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9" name="platform/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9" groups="pdk,linux,x86" clone-depth="1" />
+ <project path="prebuilts/gdb/darwin-x86" name="platform/prebuilts/gdb/darwin-x86" groups="darwin" clone-depth="1" />
+ <project path="prebuilts/gdb/linux-x86" name="platform/prebuilts/gdb/linux-x86" groups="linux" clone-depth="1" />
+ <project path="prebuilts/go/darwin-x86" name="platform/prebuilts/go/darwin-x86" groups="darwin,pdk,tradefed" clone-depth="1" />
+ <project path="prebuilts/go/linux-x86" name="platform/prebuilts/go/linux-x86" groups="linux,pdk,tradefed" clone-depth="1" />
+ <project path="prebuilts/gradle-plugin" name="platform/prebuilts/gradle-plugin" groups="pdk-cw-fs,pdk-fs" clone-depth="1" />
+ <project path="prebuilts/jdk/jdk8" name="platform/prebuilts/jdk/jdk8" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/jdk/jdk9" name="platform/prebuilts/jdk/jdk9" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/libs/libedit" name="platform/prebuilts/libs/libedit" groups="pdk-cw-fs,pdk-fs" clone-depth="1" />
+ <project path="prebuilts/maven_repo/android" name="platform/prebuilts/maven_repo/android" groups="pdk-cw-fs,pdk-fs" clone-depth="1" />
+ <project path="prebuilts/maven_repo/bumptech" name="platform/prebuilts/maven_repo/bumptech" groups="pdk-cw-fs,pdk-fs" clone-depth="1" />
+ <project path="prebuilts/maven_repo/google-play-service-client-libraries-3p" name="platform/prebuilts/maven_repo/google-play-service-client-libraries-3p" clone-depth="1" />
+ <project path="prebuilts/misc" name="platform/prebuilts/misc" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/ndk" name="platform/prebuilts/ndk" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/python/darwin-x86/2.7.5" name="platform/prebuilts/python/darwin-x86/2.7.5" groups="darwin,pdk,pdk-cw-fs,pdk-fs" clone-depth="1" />
+ <project path="prebuilts/python/linux-x86/2.7.5" name="platform/prebuilts/python/linux-x86/2.7.5" groups="linux,pdk,pdk-cw-fs,pdk-fs" clone-depth="1" />
+ <project path="prebuilts/qemu-kernel" name="platform/prebuilts/qemu-kernel" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/r8" name="platform/prebuilts/r8" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/sdk" name="platform/prebuilts/sdk" groups="pdk" clone-depth="1" />
+ <project path="prebuilts/tools" name="platform/prebuilts/tools" groups="pdk,tools" clone-depth="1" />
+ <project path="sdk" name="platform/sdk" groups="pdk-cw-fs,pdk-fs" />
+ <project path="system/bt" name="platform/system/bt" groups="pdk" />
+ <project path="system/ca-certificates" name="platform/system/ca-certificates" groups="pdk" />
+ <project path="system/chre" name="platform/system/chre" groups="pdk" />
+ <project path="system/connectivity/wificond" name="platform/system/connectivity/wificond" groups="pdk" />
+ <project path="system/connectivity/wifilogd" name="platform/system/connectivity/wifilogd" groups="pdk" />
+ <project path="system/core" name="platform/system/core" groups="pdk" />
+ <project path="system/extras" name="platform/system/extras" groups="pdk" />
+ <project path="system/gatekeeper" name="platform/system/gatekeeper" groups="pdk" />
+ <project path="system/hardware/interfaces" name="platform/system/hardware/interfaces" groups="pdk" />
+ <project path="system/hwservicemanager" name="platform/system/hwservicemanager" groups="pdk" />
+ <project path="system/iot/attestation" name="platform/system/iot/attestation" groups="pdk" />
+ <project path="system/keymaster" name="platform/system/keymaster" groups="pdk" />
+ <project path="system/libfmq" name="platform/system/libfmq" groups="pdk" />
+ <project path="system/libhidl" name="platform/system/libhidl" groups="pdk" />
+ <project path="system/libhwbinder" name="platform/system/libhwbinder" groups="pdk" />
+ <project path="system/libufdt" name="platform/system/libufdt" groups="pdk" />
+ <project path="system/libvintf" name="platform/system/libvintf" groups="pdk" />
+ <project path="system/media" name="platform/system/media" groups="pdk" />
+ <project path="system/netd" name="platform/system/netd" groups="pdk" />
+ <project path="system/nfc" name="platform/system/nfc" groups="pdk" />
+ <project path="system/nvram" name="platform/system/nvram" groups="pdk" />
+ <project path="system/security" name="platform/system/security" groups="pdk" />
+ <project path="system/sepolicy" name="platform/system/sepolicy" groups="pdk" />
+ <project path="system/timezone" name="platform/system/timezone" groups="pdk" />
+ <project path="system/tools/aidl" name="platform/system/tools/aidl" groups="pdk-cw-fs,pdk-fs" />
+ <project path="system/tools/bpt" name="platform/system/tools/bpt" groups="pdk" />
+ <project path="system/tools/hidl" name="platform/system/tools/hidl" groups="pdk" />
+ <project path="system/update_engine" name="platform/system/update_engine" groups="pdk" />
+ <project path="system/vold" name="platform/system/vold" groups="pdk" />
+ <project path="test/framework" name="platform/test/framework" groups="vts,pdk" />
+ <project path="test/mlts/benchmark" name="platform/test/mlts/benchmark" groups="pdk" />
+ <project path="test/mlts/models" name="platform/test/mlts/models" groups="pdk" />
+ <project path="test/sts" name="platform/test/sts" groups="sts,pdk" />
+ <project path="test/vti/alert" name="platform/test/vti/alert" groups="vts,pdk" />
+ <project path="test/vti/dashboard" name="platform/test/vti/dashboard" groups="vts,pdk" />
+ <project path="test/vti/fuzz_test_serving" name="platform/test/vti/fuzz_test_serving" groups="vts,pdk" />
+ <project path="test/vti/test_serving" name="platform/test/vti/test_serving" groups="vts,pdk" />
+ <project path="test/vts" name="platform/test/vts" groups="vts,pdk" />
+ <project path="test/vts-testcase/fuzz" name="platform/test/vts-testcase/fuzz" groups="vts,pdk" />
+ <project path="test/vts-testcase/hal" name="platform/test/vts-testcase/hal" groups="vts,pdk" />
+ <project path="test/vts-testcase/hal-trace" name="platform/test/vts-testcase/hal-trace" groups="vts,pdk" />
+ <project path="test/vts-testcase/kernel" name="platform/test/vts-testcase/kernel" groups="vts,pdk" />
+ <project path="test/vts-testcase/nbu" name="platform/test/vts-testcase/nbu" groups="vts,pdk" />
+ <project path="test/vts-testcase/performance" name="platform/test/vts-testcase/performance" groups="vts,pdk" />
+ <project path="test/vts-testcase/security" name="platform/test/vts-testcase/security" groups="vts,pdk" />
+ <project path="test/vts-testcase/vndk" name="platform/test/vts-testcase/vndk" groups="vts,pdk" />
+ <project path="toolchain/benchmark" name="toolchain/benchmark" />
+ <project path="toolchain/binutils" name="toolchain/binutils" groups="pdk" />
+ <project path="toolchain/pgo-profiles" name="toolchain/pgo-profiles" groups="pdk" />
+ <project path="tools/acloud" name="platform/tools/acloud" groups="tools,vts,pdk,tradefed" />
+ <project path="tools/adt/idea" name="platform/tools/adt/idea" groups="notdefault,tools" />
+ <project path="tools/apksig" name="platform/tools/apksig" groups="pdk,tradefed" />
+ <project path="tools/apkzlib" name="platform/tools/apkzlib" groups="pdk,tradefed" />
+ <project path="tools/base" name="platform/tools/base" groups="notdefault,tools" />
+ <project path="tools/build" name="platform/tools/build" groups="notdefault,tools" />
+ <project path="tools/dexter" name="platform/tools/dexter" groups="tools,pdk-fs" />
+ <project path="tools/external/fat32lib" name="platform/tools/external/fat32lib" groups="tools" />
+ <project path="tools/external/gradle" name="platform/tools/external/gradle" groups="tools" clone-depth="1" />
+ <project path="tools/idea" name="platform/tools/idea" groups="notdefault,tools" />
+ <project path="tools/loganalysis" name="platform/tools/loganalysis" groups="nopresubmit,pdk,tradefed" />
+ <project path="tools/metalava" name="platform/tools/metalava" groups="tools" />
+ <project path="tools/motodev" name="platform/tools/motodev" groups="notdefault,motodev" />
+ <project path="tools/repohooks" name="platform/tools/repohooks" groups="adt-infra,cts,developers,motodev,pdk,tools,tradefed" />
+ <project path="tools/security" name="platform/tools/security" groups="pdk,tools" />
+ <project path="tools/studio/cloud" name="platform/tools/studio/cloud" groups="notdefault,tools" />
+ <project path="tools/swt" name="platform/tools/swt" groups="notdefault,tools" />
+ <project path="tools/test/connectivity" name="platform/tools/test/connectivity" groups="pdk" />
+ <project path="tools/test/graphicsbenchmark" name="platform/tools/test/graphicsbenchmark" groups="pdk" />
+ <project path="tools/tradefederation/core" name="platform/tools/tradefederation" groups="pdk,tradefed" />
+ <project path="tools/tradefederation/contrib" name="platform/tools/tradefederation/contrib" groups="pdk,tradefed" />
+
+ <repo-hooks in-project="platform/tools/repohooks" enabled-list="pre-upload" />
+
+</manifest>
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index f76ed4bfda4..77410e0070c 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -21,27 +21,6 @@ describe IssuablesHelper do
end
end
- describe '#group_dropdown_label' do
- let(:group) { create(:group) }
- let(:default) { 'default label' }
-
- it 'returns default group label when group_id is nil' do
- expect(group_dropdown_label(nil, default)).to eq('default label')
- end
-
- it 'returns "any group" when group_id is 0' do
- expect(group_dropdown_label('0', default)).to eq('Any group')
- end
-
- it 'returns group full path when a group was found for the provided id' do
- expect(group_dropdown_label(group.id, default)).to eq(group.full_name)
- end
-
- it 'returns default label when a group was not found for the provided id' do
- expect(group_dropdown_label(9999, default)).to eq('default label')
- end
- end
-
describe '#issuable_labels_tooltip' do
it 'returns label text with no labels' do
expect(issuable_labels_tooltip([])).to eq("Labels")
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 460d3b6a7e4..343e140f5fb 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -28,6 +28,16 @@ describe NamespacesHelper do
expect(options).not_to include(admin_group.name)
expect(options).to include(user_group.name)
+ expect(options).to include(user.name)
+ end
+
+ it 'returns only groups if groups_only option is true' do
+ allow(helper).to receive(:current_user).and_return(user)
+
+ options = helper.namespaces_options(nil, groups_only: true)
+
+ expect(options).not_to include(user.name)
+ expect(options).to include(user_group.name)
end
context 'when nested groups are available', :nested_groups do
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index 05f5d47ce42..0f3a95da5bf 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
+import Vuex from 'vuex';
+import diffsModule from '~/diffs/store/modules';
+import notesModule from '~/notes/stores/modules';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
const discussionFixture = 'merge_requests/diff_discussion.json';
@@ -9,6 +12,12 @@ describe('diff_file_header', () => {
let vm;
let props;
const Component = Vue.extend(DiffFileHeader);
+ const store = new Vuex.Store({
+ modules: {
+ diffs: diffsModule,
+ notes: notesModule,
+ },
+ });
beforeEach(() => {
const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
@@ -26,13 +35,13 @@ describe('diff_file_header', () => {
describe('computed', () => {
describe('icon', () => {
beforeEach(() => {
- props.diffFile.blob.icon = 'dummy icon';
+ props.diffFile.blob.icon = 'file-text-o';
});
it('returns the blob icon for files', () => {
props.diffFile.submodule = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.icon).toBe(props.diffFile.blob.icon);
});
@@ -40,7 +49,7 @@ describe('diff_file_header', () => {
it('returns the archive icon for submodules', () => {
props.diffFile.submodule = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.icon).toBe('archive');
});
@@ -58,7 +67,7 @@ describe('diff_file_header', () => {
it('returns the fileHash for files', () => {
props.diffFile.submodule = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.titleLink).toBe(`#${props.diffFile.fileHash}`);
});
@@ -66,7 +75,7 @@ describe('diff_file_header', () => {
it('returns the submoduleTreeUrl for submodules', () => {
props.diffFile.submodule = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.titleLink).toBe(props.diffFile.submoduleTreeUrl);
});
@@ -77,7 +86,7 @@ describe('diff_file_header', () => {
submoduleTreeUrl: null,
});
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.titleLink).toBe(props.diffFile.submoduleLink);
});
@@ -94,7 +103,7 @@ describe('diff_file_header', () => {
it('returns the filePath for files', () => {
props.diffFile.submodule = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.filePath).toBe(props.diffFile.filePath);
});
@@ -102,7 +111,7 @@ describe('diff_file_header', () => {
it('appends the truncated blob id for submodules', () => {
props.diffFile.submodule = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.filePath).toBe(
`${props.diffFile.filePath} @ ${props.diffFile.blob.id.substr(0, 8)}`,
@@ -114,7 +123,7 @@ describe('diff_file_header', () => {
it('returns a link tag if fileHash is set', () => {
props.diffFile.fileHash = 'some hash';
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.titleTag).toBe('a');
});
@@ -122,7 +131,7 @@ describe('diff_file_header', () => {
it('returns a span tag if fileHash is not set', () => {
props.diffFile.fileHash = null;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.titleTag).toBe('span');
});
@@ -137,7 +146,7 @@ describe('diff_file_header', () => {
});
it('returns true if file is stored in LFS', () => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.isUsingLfs).toBe(true);
});
@@ -145,7 +154,7 @@ describe('diff_file_header', () => {
it('returns false if file is not stored externally', () => {
props.diffFile.storedExternally = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.isUsingLfs).toBe(false);
});
@@ -153,7 +162,7 @@ describe('diff_file_header', () => {
it('returns false if file is not stored in LFS', () => {
props.diffFile.externalStorage = 'not lfs';
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.isUsingLfs).toBe(false);
});
@@ -163,7 +172,7 @@ describe('diff_file_header', () => {
it('returns chevron-down if the diff is expanded', () => {
props.expanded = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.collapseIcon).toBe('chevron-down');
});
@@ -171,49 +180,18 @@ describe('diff_file_header', () => {
it('returns chevron-right if the diff is collapsed', () => {
props.expanded = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.collapseIcon).toBe('chevron-right');
});
});
- describe('isDiscussionsExpanded', () => {
- beforeEach(() => {
- Object.assign(props, {
- discussionsExpanded: true,
- expanded: true,
- });
- });
-
- it('returns true if diff and discussion are expanded', () => {
- vm = mountComponent(Component, props);
-
- expect(vm.isDiscussionsExpanded).toBe(true);
- });
-
- it('returns false if discussion is collapsed', () => {
- props.discussionsExpanded = false;
-
- vm = mountComponent(Component, props);
-
- expect(vm.isDiscussionsExpanded).toBe(false);
- });
-
- it('returns false if diff is collapsed', () => {
- props.expanded = false;
-
- vm = mountComponent(Component, props);
-
- expect(vm.isDiscussionsExpanded).toBe(false);
- });
- });
-
describe('viewFileButtonText', () => {
it('contains the truncated content SHA', () => {
const dummySha = 'deebd00f is no SHA';
props.diffFile.contentSha = dummySha;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.viewFileButtonText).not.toContain(dummySha);
expect(vm.viewFileButtonText).toContain(dummySha.substr(0, 8));
@@ -225,7 +203,7 @@ describe('diff_file_header', () => {
const dummySha = 'deadabba sings no more';
props.diffFile.diffRefs.baseSha = dummySha;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.viewReplacedFileButtonText).not.toContain(dummySha);
expect(vm.viewReplacedFileButtonText).toContain(dummySha.substr(0, 8));
@@ -234,25 +212,25 @@ describe('diff_file_header', () => {
});
describe('methods', () => {
- describe('handleToggle', () => {
+ describe('handleToggleFile', () => {
beforeEach(() => {
spyOn(vm, '$emit').and.stub();
});
it('emits toggleFile if checkTarget is false', () => {
- vm.handleToggle(null, false);
+ vm.handleToggleFile(null, false);
expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
});
it('emits toggleFile if checkTarget is true and event target is header', () => {
- vm.handleToggle({ target: vm.$refs.header }, true);
+ vm.handleToggleFile({ target: vm.$refs.header }, true);
expect(vm.$emit).toHaveBeenCalledWith('toggleFile');
});
it('does not emit toggleFile if checkTarget is true and event target is not header', () => {
- vm.handleToggle({ target: 'not header' }, true);
+ vm.handleToggleFile({ target: 'not header' }, true);
expect(vm.$emit).not.toHaveBeenCalled();
});
@@ -266,7 +244,7 @@ describe('diff_file_header', () => {
it('is visible if collapsible is true', () => {
props.collapsible = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(collapseToggle()).not.toBe(null);
});
@@ -274,14 +252,14 @@ describe('diff_file_header', () => {
it('is hidden if collapsible is false', () => {
props.collapsible = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(collapseToggle()).toBe(null);
});
});
it('displays an file icon in the title', () => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector('svg.js-file-icon use').getAttribute('xlink:href')).toContain(
'ruby',
);
@@ -293,7 +271,7 @@ describe('diff_file_header', () => {
it('displays the path of a added file', () => {
props.diffFile.renamedFile = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(filePaths()).toHaveLength(1);
expect(filePaths()[0]).toHaveText(props.diffFile.filePath);
@@ -303,7 +281,7 @@ describe('diff_file_header', () => {
props.diffFile.renamedFile = false;
props.diffFile.deletedFile = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(filePaths()).toHaveLength(1);
expect(filePaths()[0]).toHaveText(`${props.diffFile.filePath} deleted`);
@@ -312,7 +290,7 @@ describe('diff_file_header', () => {
it('displays old and new path if the file was renamed', () => {
props.diffFile.renamedFile = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(filePaths()).toHaveLength(2);
expect(filePaths()[0]).toHaveText(props.diffFile.oldPath);
@@ -321,7 +299,7 @@ describe('diff_file_header', () => {
});
it('displays a copy to clipboard button', () => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
const button = vm.$el.querySelector('.btn-clipboard');
expect(button).not.toBe(null);
@@ -332,7 +310,7 @@ describe('diff_file_header', () => {
it('it displays old and new file mode if it changed', () => {
props.diffFile.modeChanged = true;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
const { fileMode } = vm.$refs;
expect(fileMode).not.toBe(undefined);
@@ -343,7 +321,7 @@ describe('diff_file_header', () => {
it('does not display the file mode if it has not changed', () => {
props.diffFile.modeChanged = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
const { fileMode } = vm.$refs;
expect(fileMode).toBe(undefined);
@@ -359,7 +337,7 @@ describe('diff_file_header', () => {
externalStorage: 'lfs',
});
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(lfsLabel()).not.toBe(null);
expect(lfsLabel()).toHaveText('LFS');
@@ -368,7 +346,7 @@ describe('diff_file_header', () => {
it('does not display the LFS label for files stored in repository', () => {
props.diffFile.storedExternally = false;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(lfsLabel()).toBe(null);
});
@@ -376,7 +354,7 @@ describe('diff_file_header', () => {
describe('edit button', () => {
it('should not render edit button if addMergeRequestButtons is not true', () => {
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
});
@@ -384,7 +362,7 @@ describe('diff_file_header', () => {
it('should show edit button when file is editable', () => {
props.addMergeRequestButtons = true;
props.diffFile.editPath = '/';
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector('.js-edit-blob')).toContainText('Edit');
});
@@ -393,7 +371,7 @@ describe('diff_file_header', () => {
props.addMergeRequestButtons = true;
props.diffFile.deletedFile = true;
props.diffFile.editPath = '/';
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
});
@@ -413,7 +391,7 @@ describe('diff_file_header', () => {
props.diffFile.externalUrl = url;
props.diffFile.formattedExternalUrl = title;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector(`a[href="${url}"]`)).not.toBe(null);
expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).not.toBe(null);
@@ -423,11 +401,39 @@ describe('diff_file_header', () => {
props.diffFile.externalUrl = '';
props.diffFile.formattedExternalUrl = title;
- vm = mountComponent(Component, props);
+ vm = mountComponentWithStore(Component, { props, store });
expect(vm.$el.querySelector(`a[data-original-title="View on ${title}"]`)).toBe(null);
});
});
});
+
+ describe('handles toggle discussions', () => {
+ it('dispatches toggleFileDiscussions when user clicks on toggle discussions button', () => {
+ const propsCopy = Object.assign({}, props);
+ propsCopy.diffFile.submodule = false;
+ propsCopy.diffFile.blob = {
+ id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
+ path: 'lib/base.js',
+ name: 'base.js',
+ mode: '100644',
+ readableText: true,
+ icon: 'file-text-o',
+ };
+ propsCopy.addMergeRequestButtons = true;
+ propsCopy.diffFile.deletedFile = true;
+
+ vm = mountComponentWithStore(Component, {
+ props: propsCopy,
+ store,
+ });
+
+ spyOn(vm, 'toggleFileDiscussions');
+
+ vm.$el.querySelector('.js-btn-vue-toggle-comments').click();
+
+ expect(vm.toggleFileDiscussions).toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 6829c1e956a..c1560dac1a0 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -191,4 +191,48 @@ describe('DiffsStoreActions', () => {
);
});
});
+
+ describe('toggleFileDiscussions', () => {
+ it('should dispatch collapseDiscussion when all discussions are expanded', () => {
+ const getters = {
+ getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ id: 1 }]),
+ diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(true),
+ diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(false),
+ };
+
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions.toggleFileDiscussions({ getters, dispatch });
+
+ expect(dispatch).toHaveBeenCalledWith('collapseDiscussion', { discussionId: 1 }, { root: true });
+ });
+
+ it('should dispatch expandDiscussion when all discussions are collapsed', () => {
+ const getters = {
+ getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ id: 1 }]),
+ diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false),
+ diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(true),
+ };
+
+ const dispatch = jasmine.createSpy();
+
+ actions.toggleFileDiscussions({ getters, dispatch });
+
+ expect(dispatch).toHaveBeenCalledWith('expandDiscussion', { discussionId: 1 }, { root: true });
+ });
+
+ it('should dispatch expandDiscussion when some discussions are collapsed and others are expanded for the collapsed discussion', () => {
+ const getters = {
+ getDiffFileDiscussions: jasmine.createSpy().and.returnValue([{ expanded: false, id: 1 }]),
+ diffHasAllExpandedDiscussions: jasmine.createSpy().and.returnValue(false),
+ diffHasAllCollpasedDiscussions: jasmine.createSpy().and.returnValue(false),
+ };
+
+ const dispatch = jasmine.createSpy();
+
+ actions.toggleFileDiscussions({ getters, dispatch });
+
+ expect(dispatch).toHaveBeenCalledWith('expandDiscussion', { discussionId: 1 }, { root: true });
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 7a94f18778b..919b612bb6a 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -1,12 +1,24 @@
import * as getters from '~/diffs/store/getters';
import state from '~/diffs/store/modules/diff_state';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+import discussion from '../mock_data/diff_discussions';
-describe('DiffsStoreGetters', () => {
+describe('Diffs Module Getters', () => {
let localState;
+ let discussionMock;
+ let discussionMock1;
+
+ const diffFileMock = {
+ fileHash: '9732849daca6ae818696d9575f5d1207d1a7f8bb',
+ };
beforeEach(() => {
localState = state();
+ discussionMock = Object.assign({}, discussion);
+ discussionMock.diff_file.file_hash = diffFileMock.fileHash;
+
+ discussionMock1 = Object.assign({}, discussion);
+ discussionMock1.diff_file.file_hash = diffFileMock.fileHash;
});
describe('isParallelView', () => {
@@ -63,4 +75,113 @@ describe('DiffsStoreGetters', () => {
expect(getters.commitId(localState)).toEqual(null);
});
});
+
+ describe('diffHasAllExpandedDiscussions', () => {
+ it('returns true when all discussions are expanded', () => {
+ expect(
+ getters.diffHasAllExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock],
+ })(diffFileMock),
+ ).toEqual(true);
+ });
+
+ it('returns false when there are no discussions', () => {
+ expect(
+ getters.diffHasAllExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+
+ it('returns false when one discussions is collapsed', () => {
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasAllExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock1],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+ });
+
+ describe('diffHasAllCollpasedDiscussions', () => {
+ it('returns true when all discussions are collapsed', () => {
+ discussionMock.diff_file.file_hash = diffFileMock.fileHash;
+ discussionMock.expanded = false;
+
+ expect(
+ getters.diffHasAllCollpasedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock],
+ })(diffFileMock),
+ ).toEqual(true);
+ });
+
+ it('returns false when there are no discussions', () => {
+ expect(
+ getters.diffHasAllCollpasedDiscussions(localState, {
+ getDiffFileDiscussions: () => [],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+
+ it('returns false when one discussions is expanded', () => {
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasAllCollpasedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock1],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+ });
+
+ describe('diffHasExpandedDiscussions', () => {
+ it('returns true when one of the discussions is expanded', () => {
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock],
+ })(diffFileMock),
+ ).toEqual(true);
+ });
+
+ it('returns false when there are no discussions', () => {
+ expect(
+ getters.diffHasExpandedDiscussions(localState, { getDiffFileDiscussions: () => [] })(
+ diffFileMock,
+ ),
+ ).toEqual(false);
+ });
+
+ it('returns false when no discussion is expanded', () => {
+ discussionMock.expanded = false;
+ discussionMock1.expanded = false;
+
+ expect(
+ getters.diffHasExpandedDiscussions(localState, {
+ getDiffFileDiscussions: () => [discussionMock, discussionMock1],
+ })(diffFileMock),
+ ).toEqual(false);
+ });
+ });
+
+ describe('getDiffFileDiscussions', () => {
+ it('returns an array with discussions when fileHash matches and the discussion belongs to a diff', () => {
+ discussionMock.diff_file.file_hash = diffFileMock.fileHash;
+
+ expect(
+ getters.getDiffFileDiscussions(localState, {}, {}, { discussions: [discussionMock] })(
+ diffFileMock,
+ ).length,
+ ).toEqual(1);
+ });
+
+ it('returns an empty array when no discussions are found in the given diff', () => {
+ expect(
+ getters.getDiffFileDiscussions(localState, {}, {}, { discussions: [] })(diffFileMock)
+ .length,
+ ).toEqual(0);
+ });
+ });
});
diff --git a/spec/javascripts/frequent_items/store/actions_spec.js b/spec/javascripts/frequent_items/store/actions_spec.js
index 0cdd033d38f..0a8525e77d6 100644
--- a/spec/javascripts/frequent_items/store/actions_spec.js
+++ b/spec/javascripts/frequent_items/store/actions_spec.js
@@ -205,7 +205,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
actions.setSearchQuery,
{ query: 'test' },
mockedState,
- [{ type: types.SET_SEARCH_QUERY }],
+ [{ type: types.SET_SEARCH_QUERY, payload: { query: 'test' } }],
[{ type: 'fetchSearchedItems', payload: { query: 'test' } }],
done,
);
@@ -216,7 +216,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
actions.setSearchQuery,
null,
mockedState,
- [{ type: types.SET_SEARCH_QUERY }],
+ [{ type: types.SET_SEARCH_QUERY, payload: null }],
[{ type: 'fetchFrequentItems' }],
done,
);
diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js
index d6ab0aeeed7..4ca7015184e 100644
--- a/spec/javascripts/helpers/vuex_action_helper.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
@@ -1,71 +1,103 @@
+const noop = () => {};
+
/**
- * helper for testing action with expected mutations inspired in
+ * Helper for testing action with expected mutations inspired in
* https://vuex.vuejs.org/en/testing.html
*
+ * @param {Function} action to be tested
+ * @param {Object} payload will be provided to the action
+ * @param {Object} state will be provided to the action
+ * @param {Array} [expectedMutations=[]] mutations expected to be committed
+ * @param {Array} [expectedActions=[]] actions expected to be dispatched
+ * @param {Function} [done=noop] to be executed after the tests
+ * @return {Promise}
+ *
* @example
* testAction(
* actions.actionName, // action
- * { }, // mocked response
- * state, // state
+ * { }, // mocked payload
+ * state, //state
+ * // expected mutations
* [
* { type: types.MUTATION}
- * { type: types.MUTATION_1, payload: {}}
- * ], // mutations
+ * { type: types.MUTATION_1, payload: jasmine.any(Number)}
+ * ],
+ * // expected actions
* [
- * { type: 'actionName', payload: {}},
- * { type: 'actionName1', payload: {}}
- * ] //actions
+ * { type: 'actionName', payload: {param: 'foobar'}},
+ * { type: 'actionName1'}
+ * ]
* done,
* );
+ *
+ * @example
+ * testAction(
+ * actions.actionName, // action
+ * { }, // mocked payload
+ * state, //state
+ * [ { type: types.MUTATION} ], // expected mutations
+ * [], // expected actions
+ * ).then(done)
+ * .catch(done.fail);
*/
-export default (action, payload, state, expectedMutations, expectedActions, done) => {
- let mutationsCount = 0;
- let actionsCount = 0;
+export default (
+ action,
+ payload,
+ state,
+ expectedMutations = [],
+ expectedActions = [],
+ done = noop,
+) => {
+ const mutations = [];
+ const actions = [];
// mock commit
const commit = (type, mutationPayload) => {
- const mutation = expectedMutations[mutationsCount];
-
- expect(mutation.type).toEqual(type);
+ const mutation = { type };
- if (mutation.payload) {
- expect(mutation.payload).toEqual(mutationPayload);
+ if (typeof mutationPayload !== 'undefined') {
+ mutation.payload = mutationPayload;
}
- mutationsCount += 1;
- if (mutationsCount >= expectedMutations.length) {
- done();
- }
+ mutations.push(mutation);
};
// mock dispatch
const dispatch = (type, actionPayload) => {
- const actionExpected = expectedActions[actionsCount];
-
- expect(actionExpected.type).toEqual(type);
+ const dispatchedAction = { type };
- if (actionExpected.payload) {
- expect(actionExpected.payload).toEqual(actionPayload);
+ if (typeof actionPayload !== 'undefined') {
+ dispatchedAction.payload = actionPayload;
}
- actionsCount += 1;
- if (actionsCount >= expectedActions.length) {
- done();
- }
+ actions.push(dispatchedAction);
};
- // call the action with mocked store and arguments
- action({ commit, state, dispatch, rootState: state }, payload);
-
- // check if no mutations should have been dispatched
- if (expectedMutations.length === 0) {
- expect(mutationsCount).toEqual(0);
+ const validateResults = () => {
+ expect({
+ mutations,
+ actions,
+ }).toEqual({
+ mutations: expectedMutations,
+ actions: expectedActions,
+ });
done();
- }
+ };
- // check if no mutations should have been dispatched
- if (expectedActions.length === 0) {
- expect(actionsCount).toEqual(0);
- done();
- }
+ return new Promise((resolve, reject) => {
+ try {
+ const result = action({ commit, state, dispatch, rootState: state }, payload);
+ resolve(result);
+ } catch (e) {
+ reject(e);
+ }
+ })
+ .catch(error => {
+ validateResults();
+ throw error;
+ })
+ .then(data => {
+ validateResults();
+ return data;
+ });
};
diff --git a/spec/javascripts/helpers/vuex_action_helper_spec.js b/spec/javascripts/helpers/vuex_action_helper_spec.js
new file mode 100644
index 00000000000..8d6ad6750c0
--- /dev/null
+++ b/spec/javascripts/helpers/vuex_action_helper_spec.js
@@ -0,0 +1,141 @@
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import testAction from './vuex_action_helper';
+
+describe('VueX test helper (testAction)', () => {
+ let originalExpect;
+ let assertion;
+ let mock;
+ const noop = () => {};
+
+ beforeAll(() => {
+ mock = new MockAdapter(axios);
+ /*
+ In order to test the helper properly, we need to overwrite the jasmine `expect` helper.
+ We test that the testAction helper properly passes the dispatched actions/committed mutations
+ to the jasmine helper.
+ */
+ originalExpect = expect;
+ assertion = null;
+ global.expect = actual => ({
+ toEqual: () => {
+ originalExpect(actual).toEqual(assertion);
+ },
+ });
+ });
+
+ afterAll(() => {
+ mock.restore();
+ global.expect = originalExpect;
+ });
+
+ it('should properly pass on state and payload', () => {
+ const exampleState = { FOO: 12, BAR: 3 };
+ const examplePayload = { BAZ: 73, BIZ: 55 };
+
+ const action = ({ state }, payload) => {
+ originalExpect(state).toEqual(exampleState);
+ originalExpect(payload).toEqual(examplePayload);
+ };
+
+ assertion = { mutations: [], actions: [] };
+
+ testAction(action, examplePayload, exampleState);
+ });
+
+ describe('should work with synchronous actions', () => {
+ it('committing mutation', () => {
+ const action = ({ commit }) => {
+ commit('MUTATION');
+ };
+
+ assertion = { mutations: [{ type: 'MUTATION' }], actions: [] };
+
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
+
+ it('dispatching action', () => {
+ const action = ({ dispatch }) => {
+ dispatch('ACTION');
+ };
+
+ assertion = { actions: [{ type: 'ACTION' }], mutations: [] };
+
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
+
+ it('work with jasmine done once finished', done => {
+ assertion = { mutations: [], actions: [] };
+
+ testAction(noop, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('provide promise interface', done => {
+ assertion = { mutations: [], actions: [] };
+
+ testAction(noop, null, {}, assertion.mutations, assertion.actions)
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('should work with promise based actions (fetch action)', () => {
+ let lastError;
+ const data = { FOO: 'BAR' };
+
+ const promiseAction = ({ commit, dispatch }) => {
+ dispatch('ACTION');
+
+ return axios
+ .get(TEST_HOST)
+ .catch(error => {
+ commit('ERROR');
+ lastError = error;
+ throw error;
+ })
+ .then(() => {
+ commit('SUCCESS');
+ return data;
+ });
+ };
+
+ beforeEach(() => {
+ lastError = null;
+ });
+
+ it('work with jasmine done once finished', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(promiseAction, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('return original data of successful promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(promiseAction, null, {}, assertion.mutations, assertion.actions)
+ .then(res => {
+ originalExpect(res).toEqual(data);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('return original error of rejected promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(500, '');
+
+ assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(promiseAction, null, {}, assertion.mutations, assertion.actions)
+ .then(done.fail)
+ .catch(error => {
+ originalExpect(error).toBe(lastError);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js
index 770dca9cb0f..9895682f388 100644
--- a/spec/javascripts/ide/components/ide_status_bar_spec.js
+++ b/spec/javascripts/ide/components/ide_status_bar_spec.js
@@ -13,6 +13,7 @@ describe('ideStatusBar', () => {
store.state.currentProjectId = 'abcproject';
store.state.projects.abcproject = projectData;
+ store.state.currentBranchId = 'master';
vm = createComponentWithStore(Component, store).$mount();
});
@@ -60,4 +61,29 @@ describe('ideStatusBar', () => {
expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
});
});
+
+ describe('pipeline status', () => {
+ it('opens right sidebar on clicking icon', done => {
+ spyOn(vm, 'setRightPane');
+ Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
+ details: {
+ status: {
+ text: 'success',
+ details_path: 'test',
+ icon: 'success',
+ },
+ },
+ });
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.ide-status-pipeline button').click();
+
+ expect(vm.setRightPane).toHaveBeenCalledWith('pipelines-list');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/new_dropdown/button_spec.js b/spec/javascripts/ide/components/new_dropdown/button_spec.js
new file mode 100644
index 00000000000..ef083d06ba7
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/button_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import Button from '~/ide/components/new_dropdown/button.vue';
+
+describe('IDE new entry dropdown button component', () => {
+ let Component;
+ let vm;
+
+ beforeAll(() => {
+ Component = Vue.extend(Button);
+ });
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ label: 'Testing',
+ icon: 'doc-new',
+ });
+
+ spyOn(vm, '$emit');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders button with label', () => {
+ expect(vm.$el.textContent).toContain('Testing');
+ });
+
+ it('renders icon', () => {
+ expect(vm.$el.querySelector('.ic-doc-new')).not.toBe(null);
+ });
+
+ it('emits click event', () => {
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click');
+ });
+
+ it('hides label if showLabel is false', done => {
+ vm.showLabel = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).not.toContain('Testing');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
index 7b637f37eba..4d704b80209 100644
--- a/spec/javascripts/ide/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -13,6 +13,7 @@ describe('new dropdown component', () => {
vm = createComponentWithStore(component, store, {
branch: 'master',
path: '',
+ mouseOver: false,
});
vm.$store.state.currentProjectId = 'abcproject';
@@ -21,6 +22,8 @@ describe('new dropdown component', () => {
tree: [],
};
+ spyOn(vm, 'openNewEntryModal');
+
vm.$mount();
});
@@ -31,50 +34,23 @@ describe('new dropdown component', () => {
});
it('renders new file, upload and new directory links', () => {
- expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
- expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
- expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
+ const buttons = vm.$el.querySelectorAll('.dropdown-menu button');
+ expect(buttons[0].textContent.trim()).toBe('New file');
+ expect(buttons[1].textContent.trim()).toBe('Upload file');
+ expect(buttons[2].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
it('sets modalType to blob when new file is clicked', () => {
- vm.$el.querySelectorAll('a')[0].click();
+ vm.$el.querySelectorAll('.dropdown-menu button')[0].click();
- expect(vm.modalType).toBe('blob');
+ expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'blob', path: '' });
});
it('sets modalType to tree when new directory is clicked', () => {
- vm.$el.querySelectorAll('a')[2].click();
-
- expect(vm.modalType).toBe('tree');
- });
-
- it('opens modal when link is clicked', done => {
- vm.$el.querySelectorAll('a')[0].click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.modal')).not.toBeNull();
-
- done();
- });
- });
- });
-
- describe('hideModal', () => {
- beforeAll(done => {
- vm.openModal = true;
- Vue.nextTick(done);
- });
-
- it('closes modal after toggling', done => {
- vm.hideModal();
+ vm.$el.querySelectorAll('.dropdown-menu button')[2].click();
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.modal')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
+ expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'tree', path: '' });
});
});
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
index f362ed4db65..6dcc5880677 100644
--- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
+import { createStore } from '~/ide/stores';
import modal from '~/ide/components/new_dropdown/modal.vue';
-import createComponent from 'spec/helpers/vue_mount_component_helper';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
@@ -13,13 +14,15 @@ describe('new file modal component', () => {
['tree', 'blob'].forEach(type => {
describe(type, () => {
beforeEach(() => {
- vm = createComponent(Component, {
+ const store = createStore();
+ store.state.newEntryModal = {
type,
- branchId: 'master',
path: '',
- });
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
- vm.entryName = 'testing';
+ vm.name = 'testing';
});
it(`sets modal title as ${type}`, () => {
@@ -40,12 +43,11 @@ describe('new file modal component', () => {
describe('createEntryInStore', () => {
it('$emits create', () => {
- spyOn(vm, '$emit');
+ spyOn(vm, 'createTempEntry');
vm.createEntryInStore();
- expect(vm.$emit).toHaveBeenCalledWith('create', {
- branchId: 'master',
+ expect(vm.createTempEntry).toHaveBeenCalledWith({
name: 'testing',
type,
});
@@ -53,22 +55,4 @@ describe('new file modal component', () => {
});
});
});
-
- it('focuses field on mount', () => {
- document.body.innerHTML += '<div class="js-test"></div>';
-
- vm = createComponent(
- Component,
- {
- type: 'tree',
- branchId: 'master',
- path: '',
- },
- '.js-test',
- );
-
- expect(document.activeElement).toBe(vm.$refs.fieldName);
-
- vm.$el.remove();
- });
});
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
index 2bc5d701601..9c76500cfe5 100644
--- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
@@ -9,7 +9,6 @@ describe('new dropdown upload', () => {
const Component = Vue.extend(upload);
vm = createComponent(Component, {
- branchId: 'master',
path: '',
});
@@ -65,7 +64,6 @@ describe('new dropdown upload', () => {
expect(vm.$emit).toHaveBeenCalledWith('create', {
name: file.name,
- branchId: 'master',
type: 'blob',
content: target.result,
base64: false,
@@ -77,7 +75,6 @@ describe('new dropdown upload', () => {
expect(vm.$emit).toHaveBeenCalledWith('create', {
name: file.name,
- branchId: 'master',
type: 'blob',
content: binaryTarget.result.split('base64,')[1],
base64: true,
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index 80bf664d491..4bbda4c8e80 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -9,6 +9,9 @@ export const projectData = {
master: {
treeId: 'abcproject/master',
can_push: true,
+ commit: {
+ id: '123',
+ },
},
},
mergeRequests: {},
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 58d3ffc6d94..f570c0b16bd 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -601,10 +601,7 @@ describe('IDE store file actions', () => {
actions.unstageChange,
'path',
store.state,
- [
- { type: types.UNSTAGE_CHANGE, payload: 'path' },
- { type: types.SET_LAST_COMMIT_MSG, payload: '' },
- ],
+ [{ type: types.UNSTAGE_CHANGE, payload: 'path' }],
[],
done,
);
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index ca79edafb7e..6a85968e199 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -73,6 +73,7 @@ describe('IDE store project actions', () => {
branchId: store.state.currentBranchId,
},
store.state,
+ // mutations
[
{
type: 'SET_BRANCH_COMMIT',
@@ -82,17 +83,9 @@ describe('IDE store project actions', () => {
commit: { id: '123' },
},
},
- ], // mutations
- [
- {
- type: 'getLastCommitPipeline',
- payload: {
- projectId: 'abc/def',
- projectIdNumber: store.state.projects['abc/def'].id,
- branchId: 'master',
- },
- },
- ], // action
+ ],
+ // action
+ [],
done,
);
});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 6860e6cdb91..9f098eded08 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -192,11 +192,8 @@ describe('Multi-file store tree actions', () => {
showTreeEntry,
'grandparent/parent/child.txt',
store.state,
- [
- { type: types.SET_TREE_OPEN, payload: 'grandparent/parent' },
- { type: types.SET_TREE_OPEN, payload: 'grandparent' },
- ],
- [{ type: 'showTreeEntry' }],
+ [{ type: types.SET_TREE_OPEN, payload: 'grandparent/parent' }],
+ [{ type: 'showTreeEntry', payload: 'grandparent/parent' }],
done,
);
});
diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
index d21f33eaf6d..d063f1ea860 100644
--- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
@@ -122,21 +122,6 @@ describe('IDE merge requests actions', () => {
});
});
- it('dispatches request', done => {
- testAction(
- fetchMergeRequests,
- { type: 'created' },
- mockedState,
- [],
- [
- { type: 'requestMergeRequests' },
- { type: 'resetMergeRequests' },
- { type: 'receiveMergeRequestsSuccess' },
- ],
- done,
- );
- });
-
it('dispatches success with received data', done => {
testAction(
fetchMergeRequests,
@@ -144,8 +129,8 @@ describe('IDE merge requests actions', () => {
mockedState,
[],
[
- { type: 'requestMergeRequests' },
- { type: 'resetMergeRequests' },
+ { type: 'requestMergeRequests', payload: 'created' },
+ { type: 'resetMergeRequests', payload: 'created' },
{
type: 'receiveMergeRequestsSuccess',
payload: { type: 'created', data: mergeRequests },
@@ -168,9 +153,9 @@ describe('IDE merge requests actions', () => {
mockedState,
[],
[
- { type: 'requestMergeRequests' },
- { type: 'resetMergeRequests' },
- { type: 'receiveMergeRequestsError' },
+ { type: 'requestMergeRequests', payload: 'created' },
+ { type: 'resetMergeRequests', payload: 'created' },
+ { type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } },
],
done,
);
diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
index 836ba72b5d8..91edb388791 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
@@ -315,7 +315,7 @@ describe('IDE pipelines actions', () => {
'job',
mockedState,
[{ type: types.SET_DETAIL_JOB, payload: 'job' }],
- [{ type: 'setRightPane' }],
+ [{ type: 'setRightPane', payload: 'jobs-detail' }],
done,
);
});
@@ -325,7 +325,7 @@ describe('IDE pipelines actions', () => {
setDetailJob,
null,
mockedState,
- [{ type: types.SET_DETAIL_JOB }],
+ [{ type: types.SET_DETAIL_JOB, payload: null }],
[{ type: 'setRightPane', payload: rightSidebarViews.pipelines }],
done,
);
@@ -336,7 +336,7 @@ describe('IDE pipelines actions', () => {
setDetailJob,
'job',
mockedState,
- [{ type: types.SET_DETAIL_JOB }],
+ [{ type: types.SET_DETAIL_JOB, payload: 'job' }],
[{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }],
done,
);
diff --git a/spec/javascripts/issuable_time_tracker_spec.js b/spec/javascripts/issuable_time_tracker_spec.js
deleted file mode 100644
index 5add150f874..00000000000
--- a/spec/javascripts/issuable_time_tracker_spec.js
+++ /dev/null
@@ -1,201 +0,0 @@
-/* eslint-disable no-unused-vars, func-call-spacing, no-spaced-func, semi, quotes, space-infix-ops, max-len */
-
-import $ from 'jquery';
-import Vue from 'vue';
-
-import timeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
-
-function initTimeTrackingComponent(opts) {
- setFixtures(`
- <div>
- <div id="mock-container"></div>
- </div>
- `);
-
- this.initialData = {
- time_estimate: opts.timeEstimate,
- time_spent: opts.timeSpent,
- human_time_estimate: opts.timeEstimateHumanReadable,
- human_time_spent: opts.timeSpentHumanReadable,
- rootPath: '/',
- };
-
- const TimeTrackingComponent = Vue.extend(timeTracker);
- this.timeTracker = new TimeTrackingComponent({
- el: '#mock-container',
- propsData: this.initialData,
- });
-}
-
-describe('Issuable Time Tracker', function() {
- describe('Initialization', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
- });
-
- it('should return something defined', function() {
- expect(this.timeTracker).toBeDefined();
- });
-
- it ('should correctly set timeEstimate', function(done) {
- Vue.nextTick(() => {
- expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
- done();
- });
- });
- it ('should correctly set time_spent', function(done) {
- Vue.nextTick(() => {
- expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
- done();
- });
- });
- });
-
- describe('Content Display', function() {
- describe('Panes', function() {
- describe('Comparison pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
- });
-
- it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
- Vue.nextTick(() => {
- const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
- expect(this.timeTracker.showComparisonState).toBe(true);
- done();
- });
- });
-
- describe('Remaining meter', function() {
- it('should display the remaining meter with the correct width', function(done) {
- Vue.nextTick(() => {
- const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
- const correctWidth = '5%';
-
- expect(meterWidth).toBe(correctWidth);
- done();
- })
- });
-
- it('should display the remaining meter with the correct background color when within estimate', function(done) {
- Vue.nextTick(() => {
- const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
- expect(styledMeter.length).toBe(1);
- done()
- });
- });
-
- it('should display the remaining meter with the correct background color when over estimate', function(done) {
- this.timeTracker.time_estimate = 100000;
- this.timeTracker.time_spent = 20000000;
- Vue.nextTick(() => {
- const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
- expect(styledMeter.length).toBe(1);
- done();
- });
- });
- });
- });
-
- describe("Estimate only pane", function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
- });
-
- it('should display the human readable version of time estimated', function(done) {
- Vue.nextTick(() => {
- const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
- const correctText = 'Estimated: 2h 46m';
-
- expect(estimateText).toBe(correctText);
- done();
- });
- });
- });
-
- describe('Spent only pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
- });
-
- it('should display the human readable version of time spent', function(done) {
- Vue.nextTick(() => {
- const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
- const correctText = 'Spent: 1h 23m';
-
- expect(spentText).toBe(correctText);
- done();
- });
- });
- });
-
- describe('No time tracking pane', function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
- });
-
- it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
- Vue.nextTick(() => {
- const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
- const noTrackingText =$noTrackingPane.innerText;
- const correctText = 'No estimate or time spent';
-
- expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
- expect($noTrackingPane).toBeVisible();
- expect(noTrackingText).toBe(correctText);
- done();
- });
- });
- });
-
- describe("Help pane", function() {
- beforeEach(function() {
- initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
- });
-
- it('should not show the "Help" pane by default', function(done) {
- Vue.nextTick(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
-
- expect(this.timeTracker.showHelpState).toBe(false);
- expect($helpPane).toBeNull();
- done();
- });
- });
-
- it('should show the "Help" pane when help button is clicked', function(done) {
- Vue.nextTick(() => {
- $(this.timeTracker.$el).find('.help-button').click();
-
- setTimeout(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
- expect(this.timeTracker.showHelpState).toBe(true);
- expect($helpPane).toBeVisible();
- done();
- }, 10);
- });
- });
-
- it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
- Vue.nextTick(() => {
- $(this.timeTracker.$el).find('.help-button').click();
-
- setTimeout(() => {
-
- $(this.timeTracker.$el).find('.close-help-button').click();
-
- setTimeout(() => {
- const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
-
- expect(this.timeTracker.showHelpState).toBe(false);
- expect($helpPane).toBeNull();
-
- done();
- }, 1000);
- }, 1000);
- });
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 71ef3aa9b03..b66e8e1ceb3 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -128,6 +128,19 @@ describe('Actions Notes Store', () => {
});
});
+ describe('collapseDiscussion', () => {
+ it('should commit collapse discussion', done => {
+ testAction(
+ actions.collapseDiscussion,
+ { discussionId: discussionMock.id },
+ { notes: [discussionMock] },
+ [{ type: 'COLLAPSE_DISCUSSION', payload: { discussionId: discussionMock.id } }],
+ [],
+ done,
+ );
+ });
+ });
+
describe('async methods', () => {
const interceptor = (request, next) => {
next(
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index ccc7328447b..a15ff1a5888 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -74,6 +74,20 @@ describe('Notes Store mutations', () => {
});
});
+ describe('COLLAPSE_DISCUSSION', () => {
+ it('should collpase an expanded discussion', () => {
+ const discussion = Object.assign({}, discussionMock, { expanded: true });
+
+ const state = {
+ discussions: [discussion],
+ };
+
+ mutations.COLLAPSE_DISCUSSION(state, { discussionId: discussion.id });
+
+ expect(state.discussions[0].expanded).toEqual(false);
+ });
+ });
+
describe('REMOVE_PLACEHOLDER_NOTES', () => {
it('should remove all placeholder notes in indivudal notes and discussion', () => {
const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true });
diff --git a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
new file mode 100644
index 00000000000..b58de607ece
--- /dev/null
+++ b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
@@ -0,0 +1,243 @@
+import $ from 'jquery';
+import Vue from 'vue';
+
+import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('Issuable Time Tracker', () => {
+ let initialData;
+ let vm;
+
+ const initTimeTrackingComponent = opts => {
+ setFixtures(`
+ <div>
+ <div id="mock-container"></div>
+ </div>
+ `);
+
+ initialData = {
+ time_estimate: opts.timeEstimate,
+ time_spent: opts.timeSpent,
+ human_time_estimate: opts.timeEstimateHumanReadable,
+ human_time_spent: opts.timeSpentHumanReadable,
+ rootPath: '/',
+ };
+
+ const TimeTrackingComponent = Vue.extend({
+ ...TimeTracker,
+ components: {
+ ...TimeTracker.components,
+ transition: {
+ // disable animations
+ template: '<div><slot></slot></div>',
+ },
+ },
+ });
+ vm = mountComponent(TimeTrackingComponent, initialData, '#mock-container');
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Initialization', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000,
+ timeSpent: 5000,
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should return something defined', () => {
+ expect(vm).toBeDefined();
+ });
+
+ it('should correctly set timeEstimate', done => {
+ Vue.nextTick(() => {
+ expect(vm.timeEstimate).toBe(initialData.time_estimate);
+ done();
+ });
+ });
+
+ it('should correctly set time_spent', done => {
+ Vue.nextTick(() => {
+ expect(vm.timeSpent).toBe(initialData.time_spent);
+ done();
+ });
+ });
+ });
+
+ describe('Content Display', () => {
+ describe('Panes', () => {
+ describe('Comparison pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000,
+ timeSpent: 5000,
+ timeEstimateHumanReadable: '',
+ timeSpentHumanReadable: '',
+ });
+ });
+
+ it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', done => {
+ Vue.nextTick(() => {
+ expect(vm.showComparisonState).toBe(true);
+ const $comparisonPane = vm.$el.querySelector('.time-tracking-comparison-pane');
+ expect($comparisonPane).toBeVisible();
+ done();
+ });
+ });
+
+ describe('Remaining meter', () => {
+ it('should display the remaining meter with the correct width', done => {
+ Vue.nextTick(() => {
+ const meterWidth = vm.$el.querySelector('.time-tracking-comparison-pane .meter-fill')
+ .style.width;
+ const correctWidth = '5%';
+
+ expect(meterWidth).toBe(correctWidth);
+ done();
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when within estimate', done => {
+ Vue.nextTick(() => {
+ const styledMeter = $(vm.$el).find(
+ '.time-tracking-comparison-pane .within_estimate .meter-fill',
+ );
+ expect(styledMeter.length).toBe(1);
+ done();
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when over estimate', done => {
+ vm.time_estimate = 100000;
+ vm.time_spent = 20000000;
+ Vue.nextTick(() => {
+ const styledMeter = $(vm.$el).find(
+ '.time-tracking-comparison-pane .over_estimate .meter-fill',
+ );
+ expect(styledMeter.length).toBe(1);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('Estimate only pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000,
+ timeSpent: 0,
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '',
+ });
+ });
+
+ it('should display the human readable version of time estimated', done => {
+ Vue.nextTick(() => {
+ const estimateText = vm.$el.querySelector('.time-tracking-estimate-only-pane')
+ .innerText;
+ const correctText = 'Estimated: 2h 46m';
+
+ expect(estimateText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Spent only pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 0,
+ timeSpent: 5000,
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should display the human readable version of time spent', done => {
+ Vue.nextTick(() => {
+ const spentText = vm.$el.querySelector('.time-tracking-spend-only-pane').innerText;
+ const correctText = 'Spent: 1h 23m';
+
+ expect(spentText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('No time tracking pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 0,
+ timeSpent: 0,
+ timeEstimateHumanReadable: '',
+ timeSpentHumanReadable: '',
+ });
+ });
+
+ it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', done => {
+ Vue.nextTick(() => {
+ const $noTrackingPane = vm.$el.querySelector('.time-tracking-no-tracking-pane');
+ const noTrackingText = $noTrackingPane.innerText;
+ const correctText = 'No estimate or time spent';
+
+ expect(vm.showNoTimeTrackingState).toBe(true);
+ expect($noTrackingPane).toBeVisible();
+ expect(noTrackingText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Help pane', () => {
+ const helpButton = () => vm.$el.querySelector('.help-button');
+ const closeHelpButton = () => vm.$el.querySelector('.close-help-button');
+ const helpPane = () => vm.$el.querySelector('.time-tracking-help-state');
+
+ beforeEach(done => {
+ initTimeTrackingComponent({ timeEstimate: 0, timeSpent: 0 });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should not show the "Help" pane by default', () => {
+ expect(vm.showHelpState).toBe(false);
+ expect(helpPane()).toBeNull();
+ });
+
+ it('should show the "Help" pane when help button is clicked', done => {
+ helpButton().click();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.showHelpState).toBe(true);
+ expect(helpPane()).toBeVisible();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should not show the "Help" pane when help button is clicked and then closed', done => {
+ helpButton().click();
+
+ Vue.nextTick()
+ .then(() => closeHelpButton().click())
+ .then(() => Vue.nextTick())
+ .then(() => {
+ expect(vm.showHelpState).toBe(false);
+ expect(helpPane()).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js
deleted file mode 100644
index a929b804a29..00000000000
--- a/spec/javascripts/sidebar/todo_spec.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import Vue from 'vue';
-
-import SidebarTodos from '~/sidebar/components/todo_toggle/todo.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = ({
- issuableId = 1,
- issuableType = 'epic',
- isTodo,
- isActionActive,
- collapsed,
-}) => {
- const Component = Vue.extend(SidebarTodos);
-
- return mountComponent(Component, {
- issuableId,
- issuableType,
- isTodo,
- isActionActive,
- collapsed,
- });
-};
-
-describe('SidebarTodo', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent({});
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('buttonClasses', () => {
- it('returns todo button classes for when `collapsed` prop is `false`', () => {
- expect(vm.buttonClasses).toBe('btn btn-default btn-todo issuable-header-btn float-right');
- });
-
- it('returns todo button classes for when `collapsed` prop is `true`', done => {
- vm.collapsed = true;
- Vue.nextTick()
- .then(() => {
- expect(vm.buttonClasses).toBe('btn-blank btn-todo sidebar-collapsed-icon dont-change-state');
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('buttonLabel', () => {
- it('returns todo button text for marking todo as done when `isTodo` prop is `true`', () => {
- expect(vm.buttonLabel).toBe('Mark todo as done');
- });
-
- it('returns todo button text for add todo when `isTodo` prop is `false`', done => {
- vm.isTodo = false;
- Vue.nextTick()
- .then(() => {
- expect(vm.buttonLabel).toBe('Add todo');
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('collapsedButtonIconClasses', () => {
- it('returns collapsed button icon class when `isTodo` prop is `true`', () => {
- expect(vm.collapsedButtonIconClasses).toBe('todo-undone');
- });
-
- it('returns empty string when `isTodo` prop is `false`', done => {
- vm.isTodo = false;
- Vue.nextTick()
- .then(() => {
- expect(vm.collapsedButtonIconClasses).toBe('');
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('collapsedButtonIcon', () => {
- it('returns button icon name when `isTodo` prop is `true`', () => {
- expect(vm.collapsedButtonIcon).toBe('todo-done');
- });
-
- it('returns button icon name when `isTodo` prop is `false`', done => {
- vm.isTodo = false;
- Vue.nextTick()
- .then(() => {
- expect(vm.collapsedButtonIcon).toBe('todo-add');
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-
- describe('methods', () => {
- describe('handleButtonClick', () => {
- it('emits `toggleTodo` event on component', () => {
- spyOn(vm, '$emit');
- vm.handleButtonClick();
- expect(vm.$emit).toHaveBeenCalledWith('toggleTodo');
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element', () => {
- const dataAttributes = {
- issuableId: '1',
- issuableType: 'epic',
- originalTitle: 'Mark todo as done',
- placement: 'left',
- container: 'body',
- boundary: 'viewport',
- };
- expect(vm.$el.nodeName).toBe('BUTTON');
-
- const elDataAttrs = vm.$el.dataset;
- Object.keys(elDataAttrs).forEach((attr) => {
- expect(elDataAttrs[attr]).toBe(dataAttributes[attr]);
- });
- });
-
- it('renders button label element when `collapsed` prop is `false`', () => {
- const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner');
- expect(buttonLabelEl).not.toBeNull();
- expect(buttonLabelEl.innerText.trim()).toBe('Mark todo as done');
- });
-
- it('renders button icon when `collapsed` prop is `true`', done => {
- vm.collapsed = true;
- Vue.nextTick()
- .then(() => {
- const buttonIconEl = vm.$el.querySelector('svg');
- expect(buttonIconEl).not.toBeNull();
- expect(buttonIconEl.querySelector('use').getAttribute('xlink:href')).toContain('todo-done');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders loading icon when `isActionActive` prop is true', done => {
- vm.isActionActive = true;
- Vue.nextTick()
- .then(() => {
- const loadingEl = vm.$el.querySelector('span.loading-container');
- expect(loadingEl).not.toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 0eff98bcc9d..a0ca8f8b4c3 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -166,13 +166,13 @@ if (process.env.BABEL_ENV === 'coverage') {
];
describe('Uncovered files', function() {
- const sourceFiles = require.context('~', true, /\.js$/);
+ const sourceFiles = require.context('~', true, /\.(js|vue)$/);
$.holdReady(true);
sourceFiles.keys().forEach(function(path) {
// ignore if there is a matching spec file
- if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
+ if (testsContext.keys().indexOf(`${path.replace(/\.(js|vue)$/, '')}_spec`) > -1) {
return;
}
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index ec1a684cfbc..a8c5627e678 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -2,6 +2,11 @@ require "spec_helper"
describe Gitlab::Git::Branch, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:rugged) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged
+ end
+ end
subject { repository.branches }
@@ -124,6 +129,7 @@ describe Gitlab::Git::Branch, seed_helper: true do
it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
def create_commit
- repository.create_commit(params.merge(committer: committer.merge(time: Time.now)))
+ params[:message].delete!("\r")
+ Rugged::Commit.create(rugged, params.merge(committer: committer.merge(time: Time.now)))
end
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 3bb0b5be15b..7c3d2af819b 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -27,6 +27,7 @@ EOT
too_large: false
}
+ # TODO use a Gitaly diff object instead
@rugged_diff = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths:
[".gitmodules"]).patches.first
@@ -266,8 +267,12 @@ EOT
describe '#submodule?' do
before do
- commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- @diffs = commit.parents[0].diff(commit).patches
+ # TODO use a Gitaly diff object instead
+ rugged_commit = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ end
+
+ @diffs = rugged_commit.parents[0].diff(rugged_commit).patches
end
it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) }
diff --git a/spec/lib/gitlab/git/popen_spec.rb b/spec/lib/gitlab/git/popen_spec.rb
index b033ede9062..074e66d2a5d 100644
--- a/spec/lib/gitlab/git/popen_spec.rb
+++ b/spec/lib/gitlab/git/popen_spec.rb
@@ -2,6 +2,9 @@ require 'spec_helper'
describe 'Gitlab::Git::Popen' do
let(:path) { Rails.root.join('tmp').to_s }
+ let(:test_string) { 'The quick brown fox jumped over the lazy dog' }
+ # The pipe buffer is typically 64K. This string is about 440K.
+ let(:spew_command) { ['bash', '-c', "for i in {1..10000}; do echo '#{test_string}' 1>&2; done"] }
let(:klass) do
Class.new(Object) do
@@ -70,6 +73,15 @@ describe 'Gitlab::Git::Popen' do
end
end
end
+
+ context 'with a process that writes a lot of data to stderr' do
+ it 'returns zero' do
+ output, status = klass.new.popen(spew_command, path)
+
+ expect(output).to include(test_string)
+ expect(status).to eq(0)
+ end
+ end
end
context 'popen_with_timeout' do
@@ -85,6 +97,17 @@ describe 'Gitlab::Git::Popen' do
it { expect(output).to include('tests') }
end
+ context 'multi-line string' do
+ let(:test_string) { "this is 1 line\n2nd line\n3rd line\n" }
+ let(:result) { klass.new.popen_with_timeout(['echo', test_string], timeout, path) }
+ let(:output) { result.first }
+ let(:status) { result.last }
+
+ it { expect(status).to be_zero }
+ # echo adds its own line
+ it { expect(output).to eq(test_string + "\n") }
+ end
+
context 'non-zero status' do
let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) }
let(:output) { result.first }
@@ -110,6 +133,13 @@ describe 'Gitlab::Git::Popen' do
it "handles processes that do not shutdown correctly" do
expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error)
end
+
+ it 'handles process that writes a lot of data to stderr' do
+ output, status = klass.new.popen_with_timeout(spew_command, timeout, path)
+
+ expect(output).to include(test_string)
+ expect(status).to eq(0)
+ end
end
context 'timeout period' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 4f8e6f29255..64b08dd9c4b 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -611,21 +611,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#remove_remote" do
- before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- @repo.remove_remote("expendable")
- end
-
- it "should remove the remote" do
- expect(@repo.rugged.remotes).not_to include("expendable")
- end
-
- after(:all) do
- ensure_seeds
- end
- end
-
describe "#remote_update" do
before(:all) do
@repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@@ -633,7 +618,9 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it "should add the remote" do
- expect(@repo.rugged.remotes["expendable"].url).to(
+ rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access { @repo.rugged }
+
+ expect(rugged.remotes["expendable"].url).to(
eq(TEST_NORMAL_REPO_PATH)
)
end
@@ -1157,6 +1144,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
@repo.rugged.config['core.autocrlf'] = true
end
+ around do |example|
+ # OK because autocrlf is only used in gitaly-ruby
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
it 'return the value of the autocrlf option' do
expect(@repo.autocrlf).to be(true)
end
@@ -1172,6 +1166,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
@repo.rugged.config['core.autocrlf'] = false
end
+ around do |example|
+ # OK because autocrlf= is only used in gitaly-ruby
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
it 'should set the autocrlf option to the provided option' do
@repo.autocrlf = :input
@@ -1186,50 +1187,17 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#find_branch' do
- shared_examples 'finding a branch' do
- it 'should return a Branch for master' do
- branch = repository.find_branch('master')
-
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
- end
-
- it 'should handle non-existent branch' do
- branch = repository.find_branch('this-is-garbage')
-
- expect(branch).to eq(nil)
- end
- end
+ it 'should return a Branch for master' do
+ branch = repository.find_branch('master')
- context 'when Gitaly find_branch feature is enabled' do
- it_behaves_like 'finding a branch'
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
end
- context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding a branch'
-
- context 'force_reload is true' do
- it 'should reload Rugged::Repository' do
- expect(Rugged::Repository).to receive(:new).twice.and_call_original
-
- repository.find_branch('master')
- branch = repository.find_branch('master', force_reload: true)
-
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
- end
- end
-
- context 'force_reload is false' do
- it 'should not reload Rugged::Repository' do
- expect(Rugged::Repository).to receive(:new).once.and_call_original
-
- branch = repository.find_branch('master', force_reload: false)
+ it 'should handle non-existent branch' do
+ branch = repository.find_branch('this-is-garbage')
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
- end
- end
+ expect(branch).to eq(nil)
end
end
@@ -2042,54 +2010,61 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
+ let(:rugged) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.rugged }
+ end
let(:remote_name) { 'my-remote' }
+ let(:url) { 'http://my-repo.git' }
after do
ensure_seeds
end
describe '#add_remote' do
- let(:url) { 'http://my-repo.git' }
let(:mirror_refmap) { '+refs/*:refs/*' }
- it 'creates a new remote via Gitaly' do
- expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
- .to receive(:add_remote).with(remote_name, url, mirror_refmap)
+ shared_examples 'add_remote' do
+ it 'added the remote' do
+ begin
+ rugged.remotes.delete(remote_name)
+ rescue Rugged::ConfigError
+ end
- repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
+ repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
+
+ expect(rugged.remotes[remote_name]).not_to be_nil
+ expect(rugged.config["remote.#{remote_name}.mirror"]).to eq('true')
+ expect(rugged.config["remote.#{remote_name}.prune"]).to eq('true')
+ expect(rugged.config["remote.#{remote_name}.fetch"]).to eq(mirror_refmap)
+ end
end
- context 'with Gitaly disabled', :skip_gitaly_mock do
- it 'creates a new remote via Rugged' do
- expect_any_instance_of(Rugged::RemoteCollection).to receive(:create)
- .with(remote_name, url)
- expect_any_instance_of(Rugged::Config).to receive(:[]=)
- .with("remote.#{remote_name}.mirror", true)
- expect_any_instance_of(Rugged::Config).to receive(:[]=)
- .with("remote.#{remote_name}.prune", true)
- expect_any_instance_of(Rugged::Config).to receive(:[]=)
- .with("remote.#{remote_name}.fetch", mirror_refmap)
+ context 'using Gitaly' do
+ it_behaves_like 'add_remote'
+ end
- repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
- end
+ context 'with Gitaly disabled', :disable_gitaly do
+ it_behaves_like 'add_remote'
end
end
describe '#remove_remote' do
- it 'removes the remote via Gitaly' do
- expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
- .to receive(:remove_remote).with(remote_name)
+ shared_examples 'remove_remote' do
+ it 'removes the remote' do
+ rugged.remotes.create(remote_name, url)
- repository.remove_remote(remote_name)
+ repository.remove_remote(remote_name)
+
+ expect(rugged.remotes[remote_name]).to be_nil
+ end
end
- context 'with Gitaly disabled', :skip_gitaly_mock do
- it 'removes the remote via Rugged' do
- expect_any_instance_of(Rugged::RemoteCollection).to receive(:delete)
- .with(remote_name)
+ context 'using Gitaly' do
+ it_behaves_like 'remove_remote'
+ end
- repository.remove_remote(remote_name)
- end
+ context 'with Gitaly disabled', :disable_gitaly do
+ it_behaves_like 'remove_remote'
end
end
end
@@ -2281,20 +2256,25 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') }
it 'cleans up the files' do
- repository.with_worktree(worktree_path, 'master', env: ENV) do
- FileUtils.touch(worktree_path, mtime: Time.now - 8.hours)
- # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object,
- # but the HEAD must be 40 characters long or git will ignore it.
- File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA)
+ create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master]
+ raise 'preparation failed' unless system(*create_worktree, err: '/dev/null')
- # git 2.16 fails with "fatal: bad object HEAD"
- expect { repository.rev_list(including: :all) }.to raise_error(Gitlab::Git::Repository::GitError)
+ FileUtils.touch(worktree_path, mtime: Time.now - 8.hours)
+ # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object,
+ # but the HEAD must be 40 characters long or git will ignore it.
+ File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA)
- repository.clean_stale_repository_files
+ # git 2.16 fails with "fatal: bad object HEAD"
+ expect(rev_list_all).to be false
- expect { repository.rev_list(including: :all) }.not_to raise_error
- expect(File.exist?(worktree_path)).to be_falsey
- end
+ repository.clean_stale_repository_files
+
+ expect(rev_list_all).to be true
+ expect(File.exist?(worktree_path)).to be_falsey
+ end
+
+ def rev_list_all
+ system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null')
end
it 'increments a counter upon an error' do
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index b752c3e8341..d9d405e1ccc 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -32,65 +32,4 @@ describe Gitlab::Git::RevList do
expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end
end
-
- context '#new_objects' do
- it 'fetches list of newly pushed objects using rev-list' do
- stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
-
- expect { |b| rev_list.new_objects(&b) }.to yield_with_args(%w[sha1 sha2])
- end
-
- it 'can skip pathless objects' do
- stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file")
-
- expect { |b| rev_list.new_objects(require_path: true, &b) }.to yield_with_args(%w[sha2])
- end
-
- it 'can handle non utf-8 paths' do
- non_utf_char = [0x89].pack("c*").force_encoding("UTF-8")
- stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1")
-
- rev_list.new_objects(require_path: true) do |object_ids|
- expect(object_ids.force).to eq(%w[sha2])
- end
- end
-
- it 'can yield a lazy enumerator' do
- stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
-
- rev_list.new_objects do |object_ids|
- expect(object_ids).to be_a Enumerator::Lazy
- end
- end
-
- it 'returns the result of the block when given' do
- stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
-
- objects = rev_list.new_objects do |object_ids|
- object_ids.first
- end
-
- expect(objects).to eq 'sha1'
- end
-
- it 'can accept list of references to exclude' do
- stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2")
-
- expect { |b| rev_list.new_objects(not_in: ['master'], &b) }.to yield_with_args(%w[sha1 sha2])
- end
-
- it 'handles empty list of references to exclude as listing all known objects' do
- stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2")
-
- expect { |b| rev_list.new_objects(not_in: [], &b) }.to yield_with_args(%w[sha1 sha2])
- end
- end
-
- context '#all_objects' do
- it 'fetches list of all pushed objects using rev-list' do
- stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2")
-
- expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2])
- end
- end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 832db2bd906..dbd64c4bec0 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -734,26 +734,18 @@ describe Gitlab::GitAccess do
merge_into_protected_branch: "0b4bc9a #{merge_into_protected_branch} refs/heads/feature" }
end
- def stub_git_hooks
- # Running the `pre-receive` hook is expensive, and not necessary for this test.
- allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) do |service, &block|
- block.call(service)
- end
- end
-
def merge_into_protected_branch
@protected_branch_merge_commit ||= begin
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- stub_git_hooks
project.repository.add_branch(user, unprotected_branch, 'feature')
- target_branch = project.repository.lookup('feature')
+ rugged = project.repository.rugged
+ target_branch = rugged.rev_parse('feature')
source_branch = project.repository.create_file(
user,
'filename',
'This is the file content',
message: 'This is a good commit message',
branch_name: unprotected_branch)
- rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
merge_index = rugged.merge_commits(target_branch, source_branch)
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 10341486512..25827423914 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::ImportSources do
'FogBugz' => 'fogbugz',
'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project',
- 'Gitea' => 'gitea'
+ 'Gitea' => 'gitea',
+ 'Manifest file' => 'manifest'
}
expect(described_class.options).to eq(expected)
@@ -31,6 +32,7 @@ describe Gitlab::ImportSources do
git
gitlab_project
gitea
+ manifest
)
expect(described_class.values).to eq(expected)
@@ -63,7 +65,8 @@ describe Gitlab::ImportSources do
'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
- 'gitea' => Gitlab::LegacyGithubImport::Importer
+ 'gitea' => Gitlab::LegacyGithubImport::Importer,
+ 'manifest' => nil
}
import_sources.each do |name, klass|
@@ -82,7 +85,8 @@ describe Gitlab::ImportSources do
'fogbugz' => 'FogBugz',
'git' => 'Repo by URL',
'gitlab_project' => 'GitLab export',
- 'gitea' => 'Gitea'
+ 'gitea' => 'Gitea',
+ 'manifest' => 'Manifest file'
}
import_sources.each do |name, title|
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index 34b33772578..5c03a2ce7d3 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -70,4 +70,19 @@ describe Gitlab::Kubernetes do
it { is_expected.to eq(YAML.load_file(path)) }
end
end
+
+ describe '#add_terminal_auth' do
+ it 'adds authentication parameters to a hash' do
+ terminal = { original: 'value' }
+
+ add_terminal_auth(terminal, token: 'foo', max_session_time: 0, ca_pem: 'bar')
+
+ expect(terminal).to eq(
+ original: 'value',
+ headers: { 'Authorization' => ['Bearer foo'] },
+ max_session_time: 0,
+ ca_pem: 'bar'
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/manifest_import/manifest_spec.rb b/spec/lib/gitlab/manifest_import/manifest_spec.rb
new file mode 100644
index 00000000000..ab305fb2316
--- /dev/null
+++ b/spec/lib/gitlab/manifest_import/manifest_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::ManifestImport::Manifest, :postgresql do
+ let(:file) { File.open(Rails.root.join('spec/fixtures/aosp_manifest.xml')) }
+ let(:manifest) { described_class.new(file) }
+
+ describe '#valid?' do
+ context 'valid file' do
+ it { expect(manifest.valid?).to be true }
+ end
+
+ context 'missing or invalid attributes' do
+ let(:file) { Tempfile.new('foo') }
+
+ before do
+ content = <<~EOS
+ <manifest>
+ <remote review="invalid-url" />
+ <project name="platform/build"/>
+ </manifest>
+ EOS
+
+ file.write(content)
+ file.rewind
+ end
+
+ it { expect(manifest.valid?).to be false }
+
+ describe 'errors' do
+ before do
+ manifest.valid?
+ end
+
+ it { expect(manifest.errors).to include('Make sure a <remote> tag is present and is valid.') }
+ it { expect(manifest.errors).to include('Make sure every <project> tag has name and path attributes.') }
+ end
+ end
+ end
+
+ describe '#projects' do
+ it { expect(manifest.projects.size).to eq(660) }
+ it { expect(manifest.projects[0][:name]).to eq('platform/build') }
+ it { expect(manifest.projects[0][:path]).to eq('build/make') }
+ it { expect(manifest.projects[0][:url]).to eq('https://android-review.googlesource.com/platform/build') }
+ end
+end
diff --git a/spec/lib/gitlab/manifest_import/project_creator_spec.rb b/spec/lib/gitlab/manifest_import/project_creator_spec.rb
new file mode 100644
index 00000000000..1d01d437535
--- /dev/null
+++ b/spec/lib/gitlab/manifest_import/project_creator_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::ManifestImport::ProjectCreator, :postgresql do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:repository) do
+ {
+ path: 'device/common',
+ url: 'https://android-review.googlesource.com/device/common'
+ }
+ end
+
+ before do
+ group.add_owner(user)
+ end
+
+ subject { described_class.new(repository, group, user) }
+
+ describe '#execute' do
+ it { expect(subject.execute).to be_a(Project) }
+ it { expect { subject.execute }.to change { Project.count }.by(1) }
+ it { expect { subject.execute }.to change { Group.count }.by(1) }
+
+ it 'creates project with valid full path and import url' do
+ subject.execute
+
+ project = Project.last
+
+ expect(project.full_path).to eq(File.join(group.path, 'device/common'))
+ expect(project.import_url).to eq('https://android-review.googlesource.com/device/common')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index 1dbead16d5b..c1b84e9f077 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -55,6 +55,19 @@ describe Gitlab::Popen do
end
end
+ context 'with a process that writes a lot of data to stderr' do
+ let(:test_string) { 'The quick brown fox jumped over the lazy dog' }
+ # The pipe buffer is typically 64K. This string is about 440K.
+ let(:spew_command) { ['bash', '-c', "for i in {1..10000}; do echo '#{test_string}' 1>&2; done"] }
+
+ it 'returns zero' do
+ output, status = @klass.new.popen(spew_command, path)
+
+ expect(output).to include(test_string)
+ expect(status).to eq(0)
+ end
+ end
+
context 'without a directory argument' do
before do
@output, @status = @klass.new.popen(%w(ls))
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 3c96fe76829..ee923374480 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1613,6 +1613,7 @@ describe Ci::Build do
{ key: 'CI_JOB_NAME', value: 'test', public: true },
{ key: 'CI_JOB_STAGE', value: 'test', public: true },
{ key: 'CI_COMMIT_SHA', value: build.sha, public: true },
+ { key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
{ key: 'CI_BUILD_REF', value: build.sha, public: true },
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index d7c5f26ab67..77c475b9f52 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -93,4 +93,10 @@ RSpec.describe NotificationSetting do
end
end
end
+
+ context 'email events' do
+ it 'includes EXCLUDED_WATCHER_EVENTS in EMAIL_EVENTS' do
+ expect(described_class::EMAIL_EVENTS).to include(*described_class::EXCLUDED_WATCHER_EVENTS)
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c01c7bc47b5..d200e5f2e42 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -149,23 +149,25 @@ describe Project do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
-
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_length_of(:path).is_at_most(255) }
-
it { is_expected.to validate_length_of(:description).is_at_most(2000) }
-
it { is_expected.to validate_length_of(:ci_config_path).is_at_most(255) }
it { is_expected.to allow_value('').for(:ci_config_path) }
it { is_expected.not_to allow_value('test/../foo').for(:ci_config_path) }
it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) }
-
it { is_expected.to validate_presence_of(:creator) }
-
it { is_expected.to validate_presence_of(:namespace) }
-
it { is_expected.to validate_presence_of(:repository_storage) }
+ it 'validates build timeout constraints' do
+ is_expected.to validate_numericality_of(:build_timeout)
+ .only_integer
+ .is_greater_than_or_equal_to(10.minutes)
+ .is_less_than(1.month)
+ .with_message('needs to be beetween 10 minutes and 1 month')
+ end
+
it 'does not allow new projects beyond user limits' do
project2 = build(:project)
allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index a3c20b3b3c1..a544940800a 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require "spec_helper"
describe ProjectWiki do
@@ -10,7 +11,6 @@ describe ProjectWiki do
subject { project_wiki }
- it { is_expected.to delegate_method(:empty?).to :pages }
it { is_expected.to delegate_method(:repository_storage).to :project }
it { is_expected.to delegate_method(:hashed_storage?).to :project }
@@ -92,11 +92,19 @@ describe ProjectWiki do
context "when the wiki has pages" do
before do
project_wiki.create_page("index", "This is an awesome new Gollum Wiki")
+ project_wiki.create_page("another-page", "This is another page")
end
describe '#empty?' do
subject { super().empty? }
it { is_expected.to be_falsey }
+
+ # Re-enable this when https://gitlab.com/gitlab-org/gitaly/issues/1204 is fixed
+ xit 'only instantiates a Wiki page once' do
+ expect(WikiPage).to receive(:new).once.and_call_original
+
+ subject
+ end
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index caf5d829d21..02d31098cfd 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -151,7 +151,9 @@ describe Repository do
it { is_expected.to eq(['v1.1.0', 'v1.0.0', annotated_tag_name]) }
after do
- repository.rugged.tags.delete(annotated_tag_name)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged.tags.delete(annotated_tag_name)
+ end
end
end
end
@@ -1020,24 +1022,6 @@ describe Repository do
end
end
- describe '#find_branch' do
- context 'fresh_repo is true' do
- it 'delegates the call to raw_repository' do
- expect(repository.raw_repository).to receive(:find_branch).with('master', true)
-
- repository.find_branch('master', fresh_repo: true)
- end
- end
-
- context 'fresh_repo is false' do
- it 'delegates the call to raw_repository' do
- expect(repository.raw_repository).to receive(:find_branch).with('master', false)
-
- repository.find_branch('master', fresh_repo: false)
- 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
@@ -2231,8 +2215,11 @@ describe Repository do
create_remote_branch('joe', 'remote_branch', masterrev)
repository.add_branch(user, 'local_branch', masterrev.id)
- expect(repository.remote_branches('joe').any? { |branch| branch.name == 'local_branch' }).to eq(false)
- expect(repository.remote_branches('joe').any? { |branch| branch.name == 'remote_branch' }).to eq(true)
+ # TODO: move this test to gitaly https://gitlab.com/gitlab-org/gitaly/issues/1243
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ expect(repository.remote_branches('joe').any? { |branch| branch.name == 'local_branch' }).to eq(false)
+ expect(repository.remote_branches('joe').any? { |branch| branch.name == 'remote_branch' }).to eq(true)
+ end
end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index f29abcf536e..bd498269798 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -7,7 +7,6 @@ describe Todo do
it { is_expected.to belong_to(:author).class_name("User") }
it { is_expected.to belong_to(:note) }
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:target).touch(true) }
it { is_expected.to belong_to(:user) }
end
diff --git a/spec/support/shared_examples/controllers/todos_shared_examples.rb b/spec/support/shared_examples/controllers/todos_shared_examples.rb
deleted file mode 100644
index bafd9bac8d0..00000000000
--- a/spec/support/shared_examples/controllers/todos_shared_examples.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-shared_examples 'todos actions' do
- context 'when authorized' do
- before do
- sign_in(user)
- parent.add_developer(user)
- end
-
- it 'creates todo' do
- expect do
- post_create
- end.to change { user.todos.count }.by(1)
-
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'returns todo path and pending count' do
- post_create
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['count']).to eq 1
- expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}})
- end
- end
-
- context 'when not authorized for project/group' do
- it 'does not create todo for resource that user has no access to' do
- sign_in(user)
- expect do
- post_create
- end.to change { user.todos.count }.by(0)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'does not create todo when user is not logged in' do
- expect do
- post_create
- end.to change { user.todos.count }.by(0)
-
- expect(response).to have_gitlab_http_status(parent.is_a?(Group) ? 401 : 302)
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes.rb
index 79b2196660c..1b563021244 100644
--- a/spec/support/shared_examples/requests/api/notes.rb
+++ b/spec/support/shared_examples/requests/api/notes.rb
@@ -121,6 +121,7 @@ shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ expect(Time.parse(json_response['updated_at'])).to be_like_time(creation_time)
end
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index d5808e21271..30e67e67e0e 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -209,12 +209,7 @@ describe GitGarbageCollectWorker do
tree: old_commit.tree,
parents: [old_commit]
)
- Gitlab::Git::OperationService.new(nil, project.repository.raw_repository).send(
- :update_ref,
- "refs/heads/#{SecureRandom.hex(6)}",
- new_commit_sha,
- Gitlab::Git::BLANK_SHA
- )
+ rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha)
end
def packs(project)