summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-08-17 13:40:18 +0800
committerLin Jen-Shin <godfat@godfat.org>2016-08-17 13:40:18 +0800
commitf673f1e3bdbab25af50da386b29b3661d2536e95 (patch)
treed0cea7f73321ac340c4baa3e0d5d5f6f25d387e5
parent1c88ed7a515141b7468b238a679e2ff5cf86e3f9 (diff)
parent1b338d59f641ce629cbecb839b64c9fd65561276 (diff)
downloadgitlab-ce-f673f1e3bdbab25af50da386b29b3661d2536e95.tar.gz
Merge remote-tracking branch 'upstream/master' into artifacts-from-ref-and-build-name
* upstream/master: (109 commits) Update CHANGELOG for 8.10.6, 8.9.7, and 8.8.8 Updated Akismet documentation Add hover state to todos (!5361) Load issues and merge requests templates from repository Backport EE assertions in protected branch related specs. Revert "Merge branch '19957-write-tests-for-adding-comments-for-different-line-types-in-diff' into 'master'" Fix a missed `before_action` for `AutocompleteController`. Backport `AutocompleteController#load_project` from EE!581. Fix API::BranchesSpec. Fix failing tests relating to backporting ee!581. Revert unrelevant changes Fix the protected branches factory. Improve EE compatibility with protected branch access levels. Move the "update" portion of the protected branch view into a partial. Don't select an access level if already selected. Backport changes from gitlab-org/gitlab-ee!581 to CE. Further refactor and syntax fixes. Upgrade httpclient gem from 2.7.0.1 to 2.8.2. Make rubocop happy Make rubocop happy ...
-rw-r--r--CHANGELOG23
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/api.js51
-rw-r--r--app/assets/javascripts/application.js1
-rw-r--r--app/assets/javascripts/blob/template_selector.js22
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/files_comment_button.js2
-rw-r--r--app/assets/javascripts/labels_select.js12
-rw-r--r--app/assets/javascripts/protected_branch_create.js.es64
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es610
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es651
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es629
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss7
-rw-r--r--app/assets/stylesheets/framework/highlight.scss15
-rw-r--r--app/assets/stylesheets/pages/issuable.scss9
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss47
-rw-r--r--app/controllers/admin/spam_logs_controller.rb10
-rw-r--r--app/controllers/autocomplete_controller.rb18
-rw-r--r--app/controllers/concerns/service_params.rb19
-rw-r--r--app/controllers/concerns/spammable_actions.rb25
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb5
-rw-r--r--app/controllers/projects/badges_controller.rb17
-rw-r--r--app/controllers/projects/blob_controller.rb14
-rw-r--r--app/controllers/projects/hooks_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb8
-rw-r--r--app/controllers/projects/protected_branches_controller.rb26
-rw-r--r--app/controllers/projects/templates_controller.rb19
-rw-r--r--app/helpers/blob_helper.rb41
-rw-r--r--app/helpers/sorting_helper.rb8
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/pipeline.rb16
-rw-r--r--app/models/concerns/protected_branch_access.rb7
-rw-r--r--app/models/concerns/spammable.rb52
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb1
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/project_services/builds_email_service.rb3
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb11
-rw-r--r--app/models/protected_branch/merge_access_level.rb6
-rw-r--r--app/models/protected_branch/push_access_level.rb6
-rw-r--r--app/models/service.rb7
-rw-r--r--app/models/spam_log.rb4
-rw-r--r--app/models/user_agent_detail.rb9
-rw-r--r--app/services/akismet_service.rb79
-rw-r--r--app/services/create_spam_log_service.rb13
-rw-r--r--app/services/delete_branch_service.rb9
-rw-r--r--app/services/delete_tag_service.rb9
-rw-r--r--app/services/files/base_service.rb1
-rw-r--r--app/services/files/update_service.rb23
-rw-r--r--app/services/git_push_service.rb26
-rw-r--r--app/services/git_tag_push_service.rb20
-rw-r--r--app/services/ham_service.rb26
-rw-r--r--app/services/issues/create_service.rb35
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/protected_branches/create_service.rb18
-rw-r--r--app/services/spam_check_service.rb38
-rw-r--r--app/services/spam_service.rb78
-rw-r--r--app/services/test_hook_service.rb2
-rw-r--r--app/services/user_agent_detail_service.rb13
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml5
-rw-r--r--app/views/dashboard/todos/_todo.html.haml15
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml9
-rw-r--r--app/views/projects/hooks/_project_hook.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml9
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/_badge.html.haml27
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml25
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml9
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml13
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml10
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml24
-rw-r--r--app/views/shared/web_hooks/_form.html.haml21
-rw-r--r--config/routes.rb17
-rw-r--r--db/fixtures/development/14_builds.rb59
-rw-r--r--db/migrate/20160727163552_create_user_agent_details.rb18
-rw-r--r--db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb16
-rw-r--r--db/migrate/20160728103734_add_pipeline_events_to_services.rb16
-rw-r--r--db/migrate/20160729173930_remove_project_id_from_spam_logs.rb29
-rw-r--r--db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb20
-rw-r--r--db/schema.rb14
-rw-r--r--doc/ci/pipelines.md35
-rw-r--r--doc/ci/quick_start/README.md12
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/adding_database_indexes.md123
-rw-r--r--doc/development/ui_guide.md16
-rw-r--r--doc/integration/akismet.md27
-rw-r--r--doc/integration/img/spam_log.pngbin0 -> 187190 bytes
-rw-r--r--doc/integration/img/submit_issue.pngbin0 -> 174556 bytes
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/web_hooks/web_hooks.md168
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/description_templates.md12
-rw-r--r--doc/workflow/img/description_templates.pngbin0 -> 57670 bytes
-rw-r--r--features/dashboard/new_project.feature2
-rw-r--r--features/steps/dashboard/new_project.rb3
-rw-r--r--lib/api/branches.rb29
-rw-r--r--lib/api/entities.rb12
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/templates.rb26
-rw-r--r--lib/gitlab/akismet_helper.rb47
-rw-r--r--lib/gitlab/badge/base.rb21
-rw-r--r--lib/gitlab/badge/build.rb30
-rw-r--r--lib/gitlab/badge/build/metadata.rb22
-rw-r--r--lib/gitlab/badge/build/status.rb37
-rw-r--r--lib/gitlab/badge/build/template.rb32
-rw-r--r--lib/gitlab/badge/coverage/metadata.rb30
-rw-r--r--lib/gitlab/badge/coverage/report.rb56
-rw-r--r--lib/gitlab/badge/coverage/template.rb52
-rw-r--r--lib/gitlab/badge/metadata.rb36
-rw-r--r--lib/gitlab/badge/template.rb49
-rw-r--r--lib/gitlab/data_builder/build.rb (renamed from lib/gitlab/build_data_builder.rb)6
-rw-r--r--lib/gitlab/data_builder/note.rb (renamed from lib/gitlab/note_data_builder.rb)6
-rw-r--r--lib/gitlab/data_builder/pipeline.rb62
-rw-r--r--lib/gitlab/data_builder/push.rb (renamed from lib/gitlab/push_data_builder.rb)6
-rw-r--r--lib/gitlab/import_export/json_hash_builder.rb9
-rw-r--r--lib/gitlab/template/base_template.rb71
-rw-r--r--lib/gitlab/template/finders/base_template_finder.rb35
-rw-r--r--lib/gitlab/template/finders/global_template_finder.rb38
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb59
-rw-r--r--lib/gitlab/template/gitignore_template.rb (renamed from lib/gitlab/template/gitignore.rb)6
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb (renamed from lib/gitlab/template/gitlab_ci_yml.rb)6
-rw-r--r--lib/gitlab/template/issue_template.rb19
-rw-r--r--lib/gitlab/template/merge_request_template.rb19
-rw-r--r--lib/gitlab/user_access.rb4
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb5
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb12
-rw-r--r--spec/controllers/groups_controller_spec.rb5
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb50
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb48
-rw-r--r--spec/factories/project_hooks.rb10
-rw-r--r--spec/factories/protected_branches.rb12
-rw-r--r--spec/factories/user_agent_details.rb7
-rw-r--r--spec/features/projects/badges/coverage_spec.rb73
-rw-r--r--spec/features/projects/badges/list_spec.rb46
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb34
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb4
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb98
-rw-r--r--spec/features/projects/issuable_templates_spec.rb89
-rw-r--r--spec/features/protected_branches_spec.rb29
-rw-r--r--spec/helpers/notes_helper_spec.rb5
-rw-r--r--spec/lib/gitlab/akismet_helper_spec.rb35
-rw-r--r--spec/lib/gitlab/badge/build/metadata_spec.rb36
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb (renamed from spec/lib/gitlab/badge/build_spec.rb)38
-rw-r--r--spec/lib/gitlab/badge/build/template_spec.rb22
-rw-r--r--spec/lib/gitlab/badge/coverage/metadata_spec.rb30
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb93
-rw-r--r--spec/lib/gitlab/badge/coverage/template_spec.rb130
-rw-r--r--spec/lib/gitlab/badge/shared/metadata.rb21
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb (renamed from spec/lib/gitlab/build_data_builder_spec.rb)4
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb (renamed from spec/lib/gitlab/note_data_builder_spec.rb)4
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb36
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb (renamed from spec/lib/gitlab/push_data_builder_spec.rb)2
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb3
-rw-r--r--spec/lib/gitlab/template/gitignore_template_spec.rb (renamed from spec/lib/gitlab/template/gitignore_spec.rb)4
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb41
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb89
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb89
-rw-r--r--spec/models/build_spec.rb64
-rw-r--r--spec/models/ci/pipeline_spec.rb87
-rw-r--r--spec/models/concerns/spammable_spec.rb33
-rw-r--r--spec/models/project_services/assembla_service_spec.rb2
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb8
-rw-r--r--spec/models/project_services/campfire_service_spec.rb2
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb4
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb2
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb2
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb24
-rw-r--r--spec/models/project_services/irker_service_spec.rb11
-rw-r--r--spec/models/project_services/jira_service_spec.rb2
-rw-r--r--spec/models/project_services/pushover_service_spec.rb4
-rw-r--r--spec/models/project_services/slack_service_spec.rb14
-rw-r--r--spec/models/project_spec.rb6
-rw-r--r--spec/models/user_agent_detail_spec.rb31
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb2
-rw-r--r--spec/requests/api/issues_spec.rb5
-rw-r--r--spec/requests/api/project_hooks_spec.rb8
-rw-r--r--spec/requests/api/templates_spec.rb65
-rw-r--r--spec/services/files/update_service_spec.rb84
-rw-r--r--spec/services/git_push_service_spec.rb12
-rw-r--r--spec/support/import_export/import_export.yml4
-rw-r--r--spec/workers/build_email_worker_spec.rb2
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb2
191 files changed, 3532 insertions, 792 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 732f5f9436c..5924a9c6901 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.11.0 (unreleased)
+ - Add test coverage report badge. !5708
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
@@ -40,6 +41,8 @@ v 8.11.0 (unreleased)
- Various redundant database indexes have been removed
- Update `timeago` plugin to use multiple string/locale settings
- Remove unused images (ClemMakesApps)
+ - Get issue and merge request description templates from repositories
+ - Add hover state to todos !5361 (winniehell)
- Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs
- Clean up unused routes (Josef Strzibny)
@@ -73,6 +76,7 @@ v 8.11.0 (unreleased)
- The overhead of instrumented method calls has been reduced
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
+ - Add pipeline events hook
- Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell)
@@ -82,6 +86,7 @@ v 8.11.0 (unreleased)
- Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
- Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
- Fix search for notes which belongs to deleted objects
+ - Allow Akismet to be trained by submitting issues as spam or ham !5538
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem
@@ -114,6 +119,13 @@ v 8.11.0 (unreleased)
- Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- Speed up todos queries by limiting the projects set we join with
+ - Ensure file editing in UI does not overwrite commited changes without warning user
+
+v 8.10.6
+ - Upgrade Rails to 4.2.7.1 for security fixes. !5781
+ - Restore "Largest repository" sort option on Admin > Projects page. !5797
+ - Fix privilege escalation via project export.
+ - Require administrator privileges to perform a project import.
v 8.10.5
- Add a data migration to fix some missing timestamps in the members table. !5670
@@ -135,6 +147,9 @@ v 8.10.3
- Fix importer for GitHub Pull Requests when a branch was removed. !5573
- Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584
- Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
+ - Fix label already exist error message in the right sidebar.
+
+v 8.10.3 (unreleased)
v 8.10.2
- User can now search branches by name. !5144
@@ -282,6 +297,7 @@ v 8.10.0
- Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
- RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
- Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
+ - Made project list visibility icon fixed width
- Set import_url validation to be more strict
- Memoize MR merged/closed events retrieval
- Don't render discussion notes when requesting diff tab through AJAX
@@ -328,6 +344,10 @@ v 8.10.0
- Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page
+v 8.9.7
+ - Upgrade Rails to 4.2.7.1 for security fixes. !5781
+ - Require administrator privileges to perform a project import.
+
v 8.9.6
- Fix importing of events under notes for GitLab projects. !5154
- Fix log statements in import/export. !5129
@@ -593,6 +613,9 @@ v 8.9.0
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
+v 8.8.8
+ - Upgrade Rails to 4.2.7.1 for security fixes. !5781
+
v 8.8.7
- Fix privilege escalation issue with OAuth external users.
- Ensure references to private repos aren't shown to logged-out users.
diff --git a/Gemfile.lock b/Gemfile.lock
index 3ba6048143c..2244c20203b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -338,7 +338,7 @@ GEM
httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
- httpclient (2.7.0.1)
+ httpclient (2.8.2)
i18n (0.7.0)
ice_nine (0.11.1)
influxdb (0.2.3)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 49c2ac0dac3..84b292e59c6 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -9,10 +9,11 @@
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
+ issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
+
group: function(group_id, callback) {
- var url;
- url = Api.buildUrl(Api.groupPath);
- url = url.replace(':id', group_id);
+ var url = Api.buildUrl(Api.groupPath)
+ .replace(':id', group_id);
return $.ajax({
url: url,
data: {
@@ -24,8 +25,7 @@
});
},
groups: function(query, skip_ldap, callback) {
- var url;
- url = Api.buildUrl(Api.groupsPath);
+ var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
url: url,
data: {
@@ -39,8 +39,7 @@
});
},
namespaces: function(query, callback) {
- var url;
- url = Api.buildUrl(Api.namespacesPath);
+ var url = Api.buildUrl(Api.namespacesPath);
return $.ajax({
url: url,
data: {
@@ -54,8 +53,7 @@
});
},
projects: function(query, order, callback) {
- var url;
- url = Api.buildUrl(Api.projectsPath);
+ var url = Api.buildUrl(Api.projectsPath);
return $.ajax({
url: url,
data: {
@@ -70,9 +68,8 @@
});
},
newLabel: function(project_id, data, callback) {
- var url;
- url = Api.buildUrl(Api.labelsPath);
- url = url.replace(':id', project_id);
+ var url = Api.buildUrl(Api.labelsPath)
+ .replace(':id', project_id);
data.private_token = gon.api_token;
return $.ajax({
url: url,
@@ -86,9 +83,8 @@
});
},
groupProjects: function(group_id, query, callback) {
- var url;
- url = Api.buildUrl(Api.groupProjectsPath);
- url = url.replace(':id', group_id);
+ var url = Api.buildUrl(Api.groupProjectsPath)
+ .replace(':id', group_id);
return $.ajax({
url: url,
data: {
@@ -102,8 +98,8 @@
});
},
licenseText: function(key, data, callback) {
- var url;
- url = Api.buildUrl(Api.licensePath).replace(':key', key);
+ var url = Api.buildUrl(Api.licensePath)
+ .replace(':key', key);
return $.ajax({
url: url,
data: data
@@ -112,19 +108,32 @@
});
},
gitignoreText: function(key, callback) {
- var url;
- url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+ var url = Api.buildUrl(Api.gitignorePath)
+ .replace(':key', key);
return $.get(url, function(gitignore) {
return callback(gitignore);
});
},
gitlabCiYml: function(key, callback) {
- var url;
- url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+ var url = Api.buildUrl(Api.gitlabCiYmlPath)
+ .replace(':key', key);
return $.get(url, function(file) {
return callback(file);
});
},
+ issueTemplate: function(namespacePath, projectPath, key, type, callback) {
+ var url = Api.buildUrl(Api.issuableTemplatePath)
+ .replace(':key', key)
+ .replace(':type', type)
+ .replace(':project_path', projectPath)
+ .replace(':namespace_path', namespacePath);
+ $.ajax({
+ url: url,
+ dataType: 'json'
+ }).done(function(file) {
+ callback(null, file);
+ }).error(callback);
+ },
buildUrl: function(url) {
if (gon.relative_url_root != null) {
url = gon.relative_url_root + url;
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index f1aab067351..e596b98603b 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -41,6 +41,7 @@
/*= require date.format */
/*= require_directory ./behaviors */
/*= require_directory ./blob */
+/*= require_directory ./templates */
/*= require_directory ./commit */
/*= require_directory ./extensions */
/*= require_directory ./lib/utils */
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 2cf0a6631b8..b0a37ef0e0a 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -9,6 +9,7 @@
}
this.onClick = bind(this.onClick, this);
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
+ this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
@@ -60,11 +61,26 @@
return this.requestFile(item);
};
- TemplateSelector.prototype.requestFile = function(item) {};
+ TemplateSelector.prototype.requestFile = function(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ };
- TemplateSelector.prototype.requestFileSuccess = function(file) {
+ TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
- return this.editor.focus();
+ if (!skipFocus) this.editor.focus();
+ };
+
+ TemplateSelector.prototype.startLoadingSpinner = function() {
+ this.dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ };
+
+ TemplateSelector.prototype.stopLoadingSpinner = function() {
+ this.dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
};
return TemplateSelector;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 3946e861976..7160fa71ce5 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -55,6 +55,7 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
+ new IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
@@ -62,6 +63,7 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
+ new IssuableTemplateSelectors();
break;
case 'projects:tags:new':
new ZenMode();
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 09b5eb398d4..b2e49b71fec 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -33,7 +33,7 @@
this.render = bind(this.render, this);
this.VIEW_TYPE = $('input#view[type=hidden]').val();
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
- $(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
+ $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
}
FilesCommentButton.prototype.render = function(e) {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 675dd5b7cea..1bb0b67d0e8 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -70,13 +70,15 @@
name: newLabelField.val(),
color: newColorField.val()
}, function(label) {
- var errors;
$newLabelCreateButton.enable();
if (label.message != null) {
- errors = _.map(label.message, function(value, key) {
- return key + " " + value[0];
- });
- return $newLabelError.html(errors.join("<br/>")).show();
+ var errorText = label.message;
+ if (_.isObject(label.message)) {
+ errorText = _.map(label.message, function(value, key) {
+ return key + " " + value[0];
+ }).join('<br/>');
+ }
+ return $newLabelError.html(errorText).show();
} else {
return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
}
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6
index 00e20a03b04..2efca2414dc 100644
--- a/app/assets/javascripts/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branch_create.js.es6
@@ -44,8 +44,8 @@
// Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
- const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]');
- const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]');
+ const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
+ const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
this.$form.find('input[type="submit"]').removeAttr('disabled');
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
index 8d42e268ebc..a59fcbfa082 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -39,12 +39,14 @@
_method: 'PATCH',
id: this.$wrap.data('banchId'),
protected_branch: {
- merge_access_level_attributes: {
+ merge_access_levels_attributes: [{
+ id: this.$allowedToMergeDropdown.data('access-level-id'),
access_level: $allowedToMergeInput.val()
- },
- push_access_level_attributes: {
+ }],
+ push_access_levels_attributes: [{
+ id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val()
- }
+ }]
}
},
success: () => {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
new file mode 100644
index 00000000000..c32ddf80219
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -0,0 +1,51 @@
+/*= require ../blob/template_selector */
+
+((global) => {
+ class IssuableTemplateSelector extends TemplateSelector {
+ constructor(...args) {
+ super(...args);
+ this.projectPath = this.dropdown.data('project-path');
+ this.namespacePath = this.dropdown.data('namespace-path');
+ this.issuableType = this.wrapper.data('issuable-type');
+ this.titleInput = $(`#${this.issuableType}_title`);
+
+ let initialQuery = {
+ name: this.dropdown.data('selected')
+ };
+
+ if (initialQuery.name) this.requestFile(initialQuery);
+
+ $('.reset-template', this.dropdown.parent()).on('click', () => {
+ if (this.currentTemplate) this.setInputValueToTemplateContent();
+ });
+ }
+
+ requestFile(query) {
+ this.startLoadingSpinner();
+ Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
+ this.currentTemplate = currentTemplate;
+ if (err) return; // Error handled by global AJAX error handler
+ this.stopLoadingSpinner();
+ this.setInputValueToTemplateContent();
+ });
+ return;
+ }
+
+ setInputValueToTemplateContent() {
+ // `this.requestFileSuccess` sets the value of the description input field
+ // to the content of the template selected.
+ if (this.titleInput.val() === '') {
+ // If the title has not yet been set, focus the title input and
+ // skip focusing the description input by setting `true` as the 2nd
+ // argument to `requestFileSuccess`.
+ this.requestFileSuccess(this.currentTemplate, true);
+ this.titleInput.focus();
+ } else {
+ this.requestFileSuccess(this.currentTemplate);
+ }
+ return;
+ }
+ }
+
+ global.IssuableTemplateSelector = IssuableTemplateSelector;
+})(window);
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
new file mode 100644
index 00000000000..bd8cdde033e
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -0,0 +1,29 @@
+((global) => {
+ class IssuableTemplateSelectors {
+ constructor(opts = {}) {
+ this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
+ this.editor = opts.editor || this.initEditor();
+
+ this.$dropdowns.each((i, dropdown) => {
+ let $dropdown = $(dropdown);
+ new IssuableTemplateSelector({
+ pattern: /(\.md)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
+ dropdown: $dropdown,
+ editor: this.editor
+ });
+ });
+ }
+
+ initEditor() {
+ let editor = $('.markdown-area');
+ // Proxy ace-editor's .setValue to jQuery's .val
+ editor.setValue = editor.val;
+ editor.getValue = editor.val;
+ return editor;
+ }
+ }
+
+ global.IssuableTemplateSelectors = IssuableTemplateSelectors;
+})(window);
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 473530cf094..f1fe1697d30 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -164,6 +164,10 @@
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
}
+ &.btn-spam {
+ @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ }
+
&.btn-danger,
&.btn-remove,
&.btn-red {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cbbff49ad9c..6281bf6f23e 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -56,9 +56,13 @@
position: absolute;
top: 50%;
right: 6px;
- margin-top: -4px;
+ margin-top: -6px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
+ &.fa-spinner {
+ font-size: 16px;
+ margin-top: -8px;
+ }
}
&:hover, {
@@ -412,6 +416,7 @@
font-size: 14px;
a {
+ cursor: pointer;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 7cf4d4fba42..07c8874bf03 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -6,11 +6,11 @@
table-layout: fixed;
pre {
- padding: 10px;
+ padding: 10px 0;
border: none;
border-radius: 0;
font-family: $monospace_font;
- font-size: $code_font_size !important;
+ font-size: $code_font_size;
line-height: $code_line_height !important;
margin: 0;
overflow: auto;
@@ -20,13 +20,20 @@
border-left: 1px solid;
code {
+ display: inline-block;
+ min-width: 100%;
font-family: $monospace_font;
- white-space: pre;
+ white-space: normal;
word-wrap: normal;
padding: 0;
.line {
- display: inline-block;
+ display: block;
+ width: 100%;
+ min-height: 19px;
+ padding-left: 10px;
+ padding-right: 10px;
+ white-space: pre;
}
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 7a50bc9c832..46c4a11aa2e 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -395,3 +395,12 @@
display: inline-block;
line-height: 18px;
}
+
+.js-issuable-selector-wrap {
+ .js-issuable-selector {
+ width: 100%;
+ }
+ @media (max-width: $screen-sm-max) {
+ margin-bottom: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cf9aa02600d..27dc2b2a1fa 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -99,7 +99,7 @@
margin-left: auto;
margin-right: auto;
margin-bottom: 15px;
- max-width: 480px;
+ max-width: 700px;
> p {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index cf16d070cfe..0340526a53a 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -20,10 +20,43 @@
}
}
-.todo {
+.todos-list > .todo {
+ // workaround because we cannot use border-colapse
+ border-top: 1px solid transparent;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+
&:hover {
+ background-color: $row-hover;
+ border-color: $row-hover-border;
cursor: pointer;
}
+
+ // overwrite border style of .content-list
+ &:last-child {
+ border-bottom: 1px solid transparent;
+
+ &:hover {
+ border-color: $row-hover-border;
+ }
+ }
+
+ .todo-actions {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ margin-left: 10px;
+ }
+
+ .todo-item {
+ -webkit-flex: auto;
+ flex: auto;
+ }
}
.todo-item {
@@ -43,8 +76,6 @@
}
.todo-body {
- margin-right: 174px;
-
.todo-note {
word-wrap: break-word;
@@ -90,6 +121,12 @@
}
@media (max-width: $screen-xs-max) {
+ .todo {
+ .avatar {
+ display: none;
+ }
+ }
+
.todo-item {
.todo-title {
white-space: normal;
@@ -98,10 +135,6 @@
margin-bottom: 10px;
}
- .avatar {
- display: none;
- }
-
.todo-body {
margin: 0;
border-left: 2px solid #ddd;
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 3a2f0185315..2abfa22712d 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController
head :ok
end
end
+
+ def mark_as_ham
+ spam_log = SpamLog.find(params[:id])
+
+ if HamService.new(spam_log).mark_as_ham!
+ redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.'
+ else
+ redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.'
+ end
+ end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index d828d163c28..e1641ba6265 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,5 +1,6 @@
class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users]
+ before_action :load_project, only: [:users]
before_action :find_users, only: [:users]
def users
@@ -55,11 +56,8 @@ class AutocompleteController < ApplicationController
def find_users
@users =
- if params[:project_id].present?
- project = Project.find(params[:project_id])
- return render_404 unless can?(current_user, :read_project, project)
-
- project.team.users
+ if @project
+ @project.team.users
elsif params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
@@ -71,4 +69,14 @@ class AutocompleteController < ApplicationController
User.none
end
end
+
+ def load_project
+ @project ||= begin
+ if params[:project_id].present?
+ project = Project.find(params[:project_id])
+ return render_404 unless can?(current_user, :read_project, project)
+ project
+ end
+ end
+ end
end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 471d15af913..a69877edfd4 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -7,11 +7,16 @@ module ServiceParams
:build_key, :server, :teamcity_url, :drone_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
- :push_events, :issues_events, :merge_requests_events, :tag_push_events,
- :note_events, :build_events, :wiki_page_events,
- :notify_only_broken_builds, :add_pusher,
- :send_from_committer_email, :disable_diffs, :external_wiki_url,
- :notify, :color,
+ # We're using `issues_events` and `merge_requests_events`
+ # in the view so we still need to explicitly state them
+ # here. `Service#event_names` would only give
+ # `issue_events` and `merge_request_events` (singular!)
+ # See app/helpers/services_helper.rb for how we
+ # make those event names plural as special case.
+ :issues_events, :merge_requests_events,
+ :notify_only_broken_builds, :notify_only_broken_pipelines,
+ :add_pusher, :send_from_committer_email, :disable_diffs,
+ :external_wiki_url, :notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
:jira_issue_transition_id]
@@ -19,9 +24,7 @@ module ServiceParams
FILTER_BLANK_PARAMS = [:password]
def service_params
- dynamic_params = []
- dynamic_params.concat(@service.event_channel_names)
-
+ dynamic_params = @service.event_channel_names + @service.event_names
service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
if service_params[:service].is_a?(Hash)
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
new file mode 100644
index 00000000000..29e243c66a3
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -0,0 +1,25 @@
+module SpammableActions
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authorize_submit_spammable!, only: :mark_as_spam
+ end
+
+ def mark_as_spam
+ if SpamService.new(spammable).mark_as_spam!
+ redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
+ else
+ redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
+ end
+ end
+
+ private
+
+ def spammable
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+ end
+
+ def authorize_submit_spammable!
+ access_denied! unless current_user.admin?
+ end
+end
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 3ec173abcdb..7d0eff37635 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,5 +1,6 @@
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
+ before_action :authenticate_admin!
def new
@namespace_id = project_params[:namespace_id]
@@ -47,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file
)
end
+
+ def authenticate_admin!
+ render_404 unless current_user.is_admin?
+ end
end
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index d0f5071d2cc..6c25cd83a24 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -4,11 +4,24 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers, except: [:index]
def build
- badge = Gitlab::Badge::Build.new(project, params[:ref])
+ build_status = Gitlab::Badge::Build::Status
+ .new(project, params[:ref])
+ render_badge build_status
+ end
+
+ def coverage
+ coverage_report = Gitlab::Badge::Coverage::Report
+ .new(project, params[:ref], params[:job])
+
+ render_badge coverage_report
+ end
+
+ private
+
+ def render_badge(badge)
respond_to do |format|
format.html { render_404 }
-
format.svg do
render 'badge', locals: { badge: badge.template }
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 19d051720e9..cdf9a04bacf 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
+ before_action :set_last_commit_sha, only: [:edit, :update]
def new
commit unless @repository.empty?
@@ -33,7 +34,6 @@ class Projects::BlobController < Projects::ApplicationController
end
def edit
- @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
blob.load_all_data!(@repository)
end
@@ -55,6 +55,10 @@ class Projects::BlobController < Projects::ApplicationController
create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+
+ rescue Files::UpdateService::FileChangedError
+ @conflict = true
+ render :edit
end
def preview
@@ -152,7 +156,8 @@ class Projects::BlobController < Projects::ApplicationController
file_path: @file_path,
commit_message: params[:commit_message],
file_content: params[:content],
- file_content_encoding: params[:encoding]
+ file_content_encoding: params[:encoding],
+ last_commit_sha: params[:last_commit_sha]
}
end
@@ -161,4 +166,9 @@ class Projects::BlobController < Projects::ApplicationController
render nothing: true
end
end
+
+ def set_last_commit_sha
+ @last_commit_sha = Gitlab::Git::Commit.
+ last_for_path(@repository, @ref, @path).sha
+ end
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index a60027ff477..b5624046387 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController
def hook_params
params.require(:hook).permit(
:build_events,
+ :pipeline_events,
:enable_ssl_verification,
:issues_events,
:merge_requests_events,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 660e0eba06f..e9fb11e8f94 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions
include ToggleAwardEmoji
include IssuableCollections
+ include SpammableActions
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
@@ -185,6 +186,7 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
+ alias_method :spammable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 75dd3648e45..9136633b87a 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -3,7 +3,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def show
@ref = params[:ref] || @project.default_branch || 'master'
- @build_badge = Gitlab::Badge::Build.new(@project, @ref).metadata
+
+ @badges = [Gitlab::Badge::Build::Status,
+ Gitlab::Badge::Coverage::Report]
+
+ @badges.map! do |badge|
+ badge.new(@project, @ref).metadata
+ end
end
def update
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index d28ec6e2eac..9a438d5512c 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -9,16 +9,16 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def index
@protected_branch = @project.protected_branches.new
- load_protected_branches_gon_variables
+ load_gon_index
end
def create
- @protected_branch = ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
+ @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
if @protected_branch.persisted?
redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
else
load_protected_branches
- load_protected_branches_gon_variables
+ load_gon_index
render :index
end
end
@@ -28,7 +28,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
end
def update
- @protected_branch = ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
+ @protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
if @protected_branch.valid?
respond_to do |format|
@@ -58,17 +58,23 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def protected_branch_params
params.require(:protected_branch).permit(:name,
- merge_access_level_attributes: [:access_level],
- push_access_level_attributes: [:access_level])
+ merge_access_levels_attributes: [:access_level, :id],
+ push_access_levels_attributes: [:access_level, :id])
end
def load_protected_branches
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
end
- def load_protected_branches_gon_variables
- gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } },
- push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } },
- merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text } } })
+ def access_levels_options
+ {
+ push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
+ merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
+ }
+ end
+
+ def load_gon_index
+ params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
+ gon.push(params.merge(access_levels_options))
end
end
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
new file mode 100644
index 00000000000..694b468c8d3
--- /dev/null
+++ b/app/controllers/projects/templates_controller.rb
@@ -0,0 +1,19 @@
+class Projects::TemplatesController < Projects::ApplicationController
+ before_action :authenticate_user!, :get_template_class
+
+ def show
+ template = @template_type.find(params[:key], project)
+
+ respond_to do |format|
+ format.json { render json: template.to_json }
+ end
+ end
+
+ private
+
+ def get_template_class
+ template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
+ @template_type = template_types[params[:template_type]]
+ render json: [], status: 404 unless @template_type
+ end
+end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 48c27828219..1cb5d847626 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -182,17 +182,42 @@ module BlobHelper
}
end
+ def selected_template(issuable)
+ templates = issuable_templates(issuable)
+ params[:issuable_template] if templates.include?(params[:issuable_template])
+ end
+
+ def can_add_template?(issuable)
+ names = issuable_templates(issuable)
+ names.empty? && can?(current_user, :push_code, @project) && !@project.private?
+ end
+
+ def merge_request_template_names
+ @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
+ end
+
+ def issue_template_names
+ @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
+ end
+
+ def issuable_templates(issuable)
+ @issuable_templates ||=
+ if issuable.is_a?(Issue)
+ issue_template_names
+ elsif issuable.is_a?(MergeRequest)
+ merge_request_template_names
+ end
+ end
+
+ def ref_project
+ @ref_project ||= @target_project || @project
+ end
+
def gitignore_names
- @gitignore_names ||=
- Gitlab::Template::Gitignore.categories.keys.map do |k|
- [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
- end.to_h
+ @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
end
def gitlab_ci_ymls
- @gitlab_ci_ymls ||=
- Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
- [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
- end.to_h
+ @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index e1c0b497550..8b138a8e69f 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -20,13 +20,19 @@ module SortingHelper
end
def projects_sort_options_hash
- {
+ options = {
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
}
+
+ if current_controller?('admin/projects')
+ options.merge!(sort_value_largest_repo => sort_title_largest_repo)
+ end
+
+ options
end
def sort_title_priority
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3d6c6ea3209..4c84f4c21c5 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -344,7 +344,7 @@ module Ci
def execute_hooks
return unless project
- build_data = Gitlab::BuildDataBuilder.build(self)
+ build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
project.running_or_pending_build_count(force: true)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index eec99406b41..a45cf416042 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -19,6 +19,8 @@ module Ci
after_save :keep_around_commits
+ delegate :stages, to: :statuses
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
@@ -56,6 +58,10 @@ module Ci
before_transition do |pipeline|
pipeline.update_duration
end
+
+ after_transition do |pipeline, transition|
+ pipeline.execute_hooks unless transition.loopback?
+ end
end
# ref can't be HEAD or SHA, can only be branch/tag name
@@ -243,8 +249,18 @@ module Ci
self.duration = statuses.latest.duration
end
+ def execute_hooks
+ data = pipeline_data
+ project.execute_hooks(data, :pipeline_hooks)
+ project.execute_services(data, :pipeline_hooks)
+ end
+
private
+ def pipeline_data
+ Gitlab::DataBuilder::Pipeline.build(self)
+ end
+
def latest_builds_status
return 'failed' unless yaml_errors.blank?
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
new file mode 100644
index 00000000000..5a7b36070e7
--- /dev/null
+++ b/app/models/concerns/protected_branch_access.rb
@@ -0,0 +1,7 @@
+module ProtectedBranchAccess
+ extend ActiveSupport::Concern
+
+ def humanize
+ self.class.human_access_levels[self.access_level]
+ end
+end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 3b8e6df2da9..ce54fe5d3bf 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -1,9 +1,32 @@
module Spammable
extend ActiveSupport::Concern
+ module ClassMethods
+ def attr_spammable(attr, options = {})
+ spammable_attrs << [attr.to_s, options]
+ end
+ end
+
included do
+ has_one :user_agent_detail, as: :subject, dependent: :destroy
+
attr_accessor :spam
+
after_validation :check_for_spam, on: :create
+
+ cattr_accessor :spammable_attrs, instance_accessor: false do
+ []
+ end
+
+ delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
+ end
+
+ def submittable_as_spam?
+ if user_agent_detail
+ user_agent_detail.submittable?
+ else
+ false
+ end
end
def spam?
@@ -13,4 +36,33 @@ module Spammable
def check_for_spam
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
end
+
+ def spam_title
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_title, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spam_description
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_description, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spammable_text
+ result = self.class.spammable_attrs.map do |attr|
+ public_send(attr.first)
+ end
+
+ result.reject(&:blank?).join("\n")
+ end
+
+ # Override in Spammable if further checks are necessary
+ def check_for_spam?
+ true
+ end
end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index ba42a8eeb70..836a75b0608 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -5,5 +5,6 @@ class ProjectHook < WebHook
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
+ scope :pipeline_hooks, -> { where(pipeline_events: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 8b87b6c3d64..f365dee3141 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
default_value_for :build_events, false
+ default_value_for :pipeline_events, false
default_value_for :enable_ssl_verification, true
scope :push_hooks, -> { where(push_events: true) }
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d62ffb21467..788611305fe 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -36,6 +36,9 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ attr_spammable :title, spam_title: true
+ attr_spammable :description, spam_description: true
+
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
@@ -262,4 +265,9 @@ class Issue < ActiveRecord::Base
def overdue?
due_date.try(:past?) || false
end
+
+ # Only issues on public projects should be checked for spam
+ def check_for_spam?
+ project.public?
+ end
end
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 5e166471077..fa66e5864b8 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -51,8 +51,7 @@ class BuildsEmailService < Service
end
def test_data(project = nil, user = nil)
- build = project.builds.last
- Gitlab::BuildDataBuilder.build(build)
+ Gitlab::DataBuilder::Build.build(project.builds.last)
end
def fields
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index a255710f577..46f70da2452 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -56,6 +56,10 @@ class ProjectWiki
end
end
+ def repository_exists?
+ !!repository.exists?
+ end
+
def empty?
pages.empty?
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 226b3f54342..6240912a6e1 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,11 +5,14 @@ class ProtectedBranch < ActiveRecord::Base
validates :name, presence: true
validates :project, presence: true
- has_one :merge_access_level, dependent: :destroy
- has_one :push_access_level, dependent: :destroy
+ has_many :merge_access_levels, dependent: :destroy
+ has_many :push_access_levels, dependent: :destroy
- accepts_nested_attributes_for :push_access_level
- accepts_nested_attributes_for :merge_access_level
+ validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+ validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+
+ accepts_nested_attributes_for :push_access_levels
+ accepts_nested_attributes_for :merge_access_levels
def commit
project.commit(self.name)
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
index b1112ee737d..806b3ccd275 100644
--- a/app/models/protected_branch/merge_access_level.rb
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -1,4 +1,6 @@
class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
+ include ProtectedBranchAccess
+
belongs_to :protected_branch
delegate :project, to: :protected_branch
@@ -17,8 +19,4 @@ class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base
project.team.max_member_access(user.id) >= access_level
end
-
- def humanize
- self.class.human_access_levels[self.access_level]
- end
end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index 6a5e49cf453..92e9c51d883 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -1,4 +1,6 @@
class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
+ include ProtectedBranchAccess
+
belongs_to :protected_branch
delegate :project, to: :protected_branch
@@ -20,8 +22,4 @@ class ProtectedBranch::PushAccessLevel < ActiveRecord::Base
project.team.max_member_access(user.id) >= access_level
end
-
- def humanize
- self.class.human_access_levels[self.access_level]
- end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 40cd9b861f0..09b4717a523 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -36,6 +36,7 @@ class Service < ActiveRecord::Base
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
+ scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
@@ -79,13 +80,17 @@ class Service < ActiveRecord::Base
end
def test_data(project, user)
- Gitlab::PushDataBuilder.build_sample(project, user)
+ Gitlab::DataBuilder::Push.build_sample(project, user)
end
def event_channel_names
[]
end
+ def event_names
+ supported_events.map { |event| "#{event}_events" }
+ end
+
def event_field(event)
nil
end
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
index 12df68ef83b..3b8b9833565 100644
--- a/app/models/spam_log.rb
+++ b/app/models/spam_log.rb
@@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base
user.block
user.destroy
end
+
+ def text
+ [title, description].join("\n")
+ end
end
diff --git a/app/models/user_agent_detail.rb b/app/models/user_agent_detail.rb
new file mode 100644
index 00000000000..0949c6ef083
--- /dev/null
+++ b/app/models/user_agent_detail.rb
@@ -0,0 +1,9 @@
+class UserAgentDetail < ActiveRecord::Base
+ belongs_to :subject, polymorphic: true
+
+ validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true
+
+ def submittable?
+ !submitted?
+ end
+end
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
new file mode 100644
index 00000000000..5c60addbe7c
--- /dev/null
+++ b/app/services/akismet_service.rb
@@ -0,0 +1,79 @@
+class AkismetService
+ attr_accessor :owner, :text, :options
+
+ def initialize(owner, text, options = {})
+ @owner = owner
+ @text = text
+ @options = options
+ end
+
+ def is_spam?
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ created_at: DateTime.now,
+ author: owner.name,
+ author_email: owner.email,
+ referrer: options[:referrer],
+ }
+
+ begin
+ is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params)
+ is_spam || is_blatant
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
+ false
+ end
+ end
+
+ def submit_ham
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ author: owner.name,
+ author_email: owner.email
+ }
+
+ begin
+ akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
+ true
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
+ false
+ end
+ end
+
+ def submit_spam
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ author: owner.name,
+ author_email: owner.email
+ }
+
+ begin
+ akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
+ true
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
+ false
+ end
+ end
+
+ private
+
+ def akismet_client
+ @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+ Gitlab.config.gitlab.url)
+ end
+
+ def akismet_enabled?
+ current_application_settings.akismet_enabled
+ end
+end
diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb
deleted file mode 100644
index 59a66fde47a..00000000000
--- a/app/services/create_spam_log_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class CreateSpamLogService < BaseService
- def initialize(project, user, params)
- super(project, user, params)
- end
-
- def execute
- spam_params = params.merge({ user_id: @current_user.id,
- project_id: @project.id } )
- spam_log = SpamLog.new(spam_params)
- spam_log.save
- spam_log
- end
-end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 87f066edb6f..918eddaa53a 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -39,7 +39,12 @@ class DeleteBranchService < BaseService
end
def build_push_data(branch)
- Gitlab::PushDataBuilder
- .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ branch.target.sha,
+ Gitlab::Git::BLANK_SHA,
+ "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
+ [])
end
end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index 32e0eed6b63..d0cb151a010 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -33,7 +33,12 @@ class DeleteTagService < BaseService
end
def build_push_data(tag)
- Gitlab::PushDataBuilder
- .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", [])
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ tag.target.sha,
+ Gitlab::Git::BLANK_SHA,
+ "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
+ [])
end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index c4a206f785e..ea94818713b 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -15,6 +15,7 @@ module Files
else
params[:file_content]
end
+ @last_commit_sha = params[:last_commit_sha]
# Validate parameters
validate
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 8d2b5083179..4fc3b640799 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -2,11 +2,34 @@ require_relative "base_service"
module Files
class UpdateService < Files::BaseService
+ class FileChangedError < StandardError; end
+
def commit
repository.update_file(current_user, @file_path, @file_content,
branch: @target_branch,
previous_path: @previous_path,
message: @commit_message)
end
+
+ private
+
+ def validate
+ super
+
+ if file_has_changed?
+ raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
+ end
+ end
+
+ def file_has_changed?
+ return false unless @last_commit_sha && last_commit
+
+ @last_commit_sha != last_commit.sha
+ end
+
+ def last_commit
+ @last_commit ||= Gitlab::Git::Commit.
+ last_for_path(@source_project.repository, @source_branch, @file_path)
+ end
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 6f521462cf3..78feb37aa2a 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -91,12 +91,12 @@ class GitPushService < BaseService
params = {
name: @project.default_branch,
- push_access_level_attributes: {
+ push_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- },
- merge_access_level_attributes: {
+ }],
+ merge_access_levels_attributes: [{
access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }
+ }]
}
ProtectedBranches::CreateService.new(@project, current_user, params).execute
@@ -138,13 +138,23 @@ class GitPushService < BaseService
end
def build_push_data
- @push_data ||= Gitlab::PushDataBuilder.
- build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
+ @push_data ||= Gitlab::DataBuilder::Push.build(
+ @project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ push_commits)
end
def build_push_data_system_hook
- @push_data_system ||= Gitlab::PushDataBuilder.
- build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], [])
+ @push_data_system ||= Gitlab::DataBuilder::Push.build(
+ @project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ [])
end
def push_to_existing_branch?
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index d2b52f16fa8..e6002b03b93 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -34,12 +34,24 @@ class GitTagPushService < BaseService
end
end
- Gitlab::PushDataBuilder.
- build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message)
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ commits,
+ message)
end
def build_system_push_data
- Gitlab::PushDataBuilder.
- build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '')
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ [],
+ '')
end
end
diff --git a/app/services/ham_service.rb b/app/services/ham_service.rb
new file mode 100644
index 00000000000..b0e1799b489
--- /dev/null
+++ b/app/services/ham_service.rb
@@ -0,0 +1,26 @@
+class HamService
+ attr_accessor :spam_log
+
+ def initialize(spam_log)
+ @spam_log = spam_log
+ end
+
+ def mark_as_ham!
+ if akismet.submit_ham
+ spam_log.update_attribute(:submitted_as_ham, true)
+ else
+ false
+ end
+ end
+
+ private
+
+ def akismet
+ @akismet ||= AkismetService.new(
+ spam_log.user,
+ spam_log.text,
+ ip_address: spam_log.source_ip,
+ user_agent: spam_log.user_agent
+ )
+ end
+end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 5e2de2ccf64..65550ab8ec6 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -3,29 +3,34 @@ module Issues
def execute
filter_params
label_params = params.delete(:label_ids)
- request = params.delete(:request)
- api = params.delete(:api)
- issue = project.issues.new(params)
- issue.author = params[:author] || current_user
+ @request = params.delete(:request)
+ @api = params.delete(:api)
+ @issue = project.issues.new(params)
+ @issue.author = params[:author] || current_user
- issue.spam = spam_check_service.execute(request, api)
+ @issue.spam = spam_service.check(@api)
- if issue.save
- issue.update_attributes(label_ids: label_params)
- notification_service.new_issue(issue, current_user)
- todo_service.new_issue(issue, current_user)
- event_service.open_issue(issue, current_user)
- issue.create_cross_references!(current_user)
- execute_hooks(issue, 'open')
+ if @issue.save
+ @issue.update_attributes(label_ids: label_params)
+ notification_service.new_issue(@issue, current_user)
+ todo_service.new_issue(@issue, current_user)
+ event_service.open_issue(@issue, current_user)
+ user_agent_detail_service.create
+ @issue.create_cross_references!(current_user)
+ execute_hooks(@issue, 'open')
end
- issue
+ @issue
end
private
- def spam_check_service
- SpamCheckService.new(project, current_user, params)
+ def spam_service
+ SpamService.new(@issue, @request)
+ end
+
+ def user_agent_detail_service
+ UserAgentDetailService.new(@issue, @request)
end
end
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index 534c48aefff..e4cd3fc7833 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -16,7 +16,7 @@ module Notes
end
def hook_data
- Gitlab::NoteDataBuilder.build(@note, @note.author)
+ Gitlab::DataBuilder::Note.build(@note, @note.author)
end
def execute_note_hooks
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index 6150a2a83c9..a84e335340d 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -5,23 +5,7 @@ module ProtectedBranches
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
- protected_branch = project.protected_branches.new(params)
-
- ProtectedBranch.transaction do
- protected_branch.save!
-
- if protected_branch.push_access_level.blank?
- protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER)
- end
-
- if protected_branch.merge_access_level.blank?
- protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER)
- end
- end
-
- protected_branch
- rescue ActiveRecord::RecordInvalid
- protected_branch
+ project.protected_branches.create(params)
end
end
end
diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb
deleted file mode 100644
index 7c3e692bde9..00000000000
--- a/app/services/spam_check_service.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-class SpamCheckService < BaseService
- include Gitlab::AkismetHelper
-
- attr_accessor :request, :api
-
- def execute(request, api)
- @request, @api = request, api
- return false unless request || check_for_spam?(project)
- return false unless is_spam?(request.env, current_user, text)
-
- create_spam_log
-
- true
- end
-
- private
-
- def text
- [params[:title], params[:description]].reject(&:blank?).join("\n")
- end
-
- def spam_log_attrs
- {
- user_id: current_user.id,
- project_id: project.id,
- title: params[:title],
- description: params[:description],
- source_ip: client_ip(request.env),
- user_agent: user_agent(request.env),
- noteable_type: 'Issue',
- via_api: api
- }
- end
-
- def create_spam_log
- CreateSpamLogService.new(project, current_user, spam_log_attrs).execute
- end
-end
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
new file mode 100644
index 00000000000..48903291799
--- /dev/null
+++ b/app/services/spam_service.rb
@@ -0,0 +1,78 @@
+class SpamService
+ attr_accessor :spammable, :request, :options
+
+ def initialize(spammable, request = nil)
+ @spammable = spammable
+ @request = request
+ @options = {}
+
+ if @request
+ @options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
+ @options[:user_agent] = @request.env['HTTP_USER_AGENT']
+ @options[:referrer] = @request.env['HTTP_REFERRER']
+ else
+ @options[:ip_address] = @spammable.ip_address
+ @options[:user_agent] = @spammable.user_agent
+ end
+ end
+
+ def check(api = false)
+ return false unless request && check_for_spam?
+
+ return false unless akismet.is_spam?
+
+ create_spam_log(api)
+ true
+ end
+
+ def mark_as_spam!
+ return false unless spammable.submittable_as_spam?
+
+ if akismet.submit_spam
+ spammable.user_agent_detail.update_attribute(:submitted, true)
+ else
+ false
+ end
+ end
+
+ private
+
+ def akismet
+ @akismet ||= AkismetService.new(
+ spammable_owner,
+ spammable.spammable_text,
+ options
+ )
+ end
+
+ def spammable_owner
+ @user ||= User.find(spammable_owner_id)
+ end
+
+ def spammable_owner_id
+ @owner_id ||=
+ if spammable.respond_to?(:author_id)
+ spammable.author_id
+ elsif spammable.respond_to?(:creator_id)
+ spammable.creator_id
+ end
+ end
+
+ def check_for_spam?
+ spammable.check_for_spam?
+ end
+
+ def create_spam_log(api)
+ SpamLog.create(
+ {
+ user_id: spammable_owner_id,
+ title: spammable.spam_title,
+ description: spammable.spam_description,
+ source_ip: options[:ip_address],
+ user_agent: options[:user_agent],
+ noteable_type: spammable.class.to_s,
+ via_api: api
+ }
+ )
+ end
+end
diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb
index e85e58751e7..280c81f7d2d 100644
--- a/app/services/test_hook_service.rb
+++ b/app/services/test_hook_service.rb
@@ -1,6 +1,6 @@
class TestHookService
def execute(hook, current_user)
- data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user)
+ data = Gitlab::DataBuilder::Push.build_sample(hook.project, current_user)
hook.execute(data, 'push_hooks')
end
end
diff --git a/app/services/user_agent_detail_service.rb b/app/services/user_agent_detail_service.rb
new file mode 100644
index 00000000000..a1ee3df5fe1
--- /dev/null
+++ b/app/services/user_agent_detail_service.rb
@@ -0,0 +1,13 @@
+class UserAgentDetailService
+ attr_accessor :spammable, :request
+
+ def initialize(spammable, request)
+ @spammable, @request = spammable, request
+ end
+
+ def create
+ return unless request
+
+ spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s)
+ end
+end
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 8aea67f4497..4ce4eab8753 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -24,6 +24,11 @@
= link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
%td
+ - if spam_log.submitted_as_ham?
+ .btn.btn-xs.disabled
+ Submitted as ham
+ - else
+ = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning'
- if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
- else
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 98f302d2f93..b40395c74de 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,6 +1,7 @@
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
+ = author_avatar(todo, size: 40)
+
.todo-item.todo-block
- = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title.title
- unless todo.build_failed?
= todo_target_state_pill(todo)
@@ -19,13 +20,13 @@
&middot; #{time_ago_with_tooltip(todo.created_at)}
- - if todo.pending?
- .todo-actions.pull-right
- = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
- Done
- = icon('spinner spin')
-
.todo-body
.todo-note
.md
= event_note(todo.body, project: todo.project)
+
+ - if todo.pending?
+ .todo-actions
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index ff379bafb26..0237e152b54 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -24,7 +24,7 @@
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
- .file-content.code
+ .file-editor.code
%pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index b1c9895f43e..7b0621f9401 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,5 +1,11 @@
- page_title "Edit", @blob.path, @ref
+- if @conflict
+ .alert.alert-danger
+ Someone edited the file the same time you did. Please check out
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+ and make sure your changes will not unintentionally remove theirs.
+
.file-editor
%ul.nav-links.no-bottom.js-edit-mode
%li.active
@@ -13,8 +19,7 @@
= form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
-
- = hidden_field_tag 'last_commit', @last_commit
+ = hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml
index 8151187d499..3fcf1692e09 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/hooks/_project_hook.html.haml
@@ -3,7 +3,7 @@
.col-md-8.col-lg-7
%strong.light-header= hook.url
%div
- - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger|
+ - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index e5cce16a171..9f1a046ea74 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -37,14 +37,19 @@
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
+ - if @issue.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
- if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do
- Edit
+ - if @issue.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index adcc984f506..ea4898f2107 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -77,7 +77,7 @@
= link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL')
%div{ class: 'import_gitlab_project' }
- - if gitlab_project_import_enabled?
+ - if gitlab_project_import_enabled? && current_user.is_admin?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
new file mode 100644
index 00000000000..7b7fa56d993
--- /dev/null
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -0,0 +1,27 @@
+.row{ class: badge.title.gsub(' ', '-') }
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = badge.title.capitalize
+ .col-lg-9
+ .prepend-top-10
+ .panel.panel-default
+ .panel-heading
+ %b
+ = badge.title.capitalize
+ &middot;
+ = badge.to_html
+ .pull-right
+ = render 'shared/ref_switcher', destination: 'badges', align_right: true
+ .panel-body
+ .row
+ .col-md-2.text-center
+ Markdown
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.md', badge.to_markdown)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ HTML
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.html', badge.to_html)
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index 228bad36ebd..8c7222bfe3d 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -77,27 +77,4 @@
%hr
.row.prepend-top-default
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- Builds Badge
- .col-lg-9
- .prepend-top-10
- .panel.panel-default
- .panel-heading
- %b Builds badge &middot;
- = @build_badge.to_html
- .pull-right
- = render 'shared/ref_switcher', destination: 'badges', align_right: true
- .panel-body
- .row
- .col-md-2.text-center
- Markdown
- .col-md-10.code.js-syntax-highlight
- = highlight('.md', @build_badge.to_markdown)
- .row
- %hr
- .row
- .col-md-2.text-center
- HTML
- .col-md-10.code.js-syntax-highlight
- = highlight('.html', @build_badge.to_html)
+ = render partial: 'badge', collection: @badges
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index 85d0c494ba8..d4c6fa24768 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -5,6 +5,7 @@
Protect a branch
.panel-body
.form-horizontal
+ = form_errors(@protected_branch)
.form-group
= f.label :name, class: 'col-md-2 text-right' do
Branch:
@@ -18,19 +19,19 @@
%code production/*
are supported
.form-group
- %label.col-md-2.text-right{ for: 'merge_access_level_attributes' }
+ %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
Allowed to merge:
.col-md-10
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-merge wide',
- data: { field_name: 'protected_branch[merge_access_level_attributes][access_level]', input_id: 'merge_access_level_attributes' }})
+ data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
.form-group
- %label.col-md-2.text-right{ for: 'push_access_level_attributes' }
+ %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
Allowed to push:
.col-md-10
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push wide',
- data: { field_name: 'protected_branch[push_access_level_attributes][access_level]', input_id: 'push_access_level_attributes' }})
+ data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
.panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index e2e01ee78f8..0628134b1bb 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -13,16 +13,9 @@
= time_ago_with_tooltip(commit.committed_date)
- else
(branch was removed from repository)
- %td
- = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level
- = dropdown_tag( (protected_branch.merge_access_level.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container',
- data: { field_name: "allowed_to_merge_#{protected_branch.id}" }})
- %td
- = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level
- = dropdown_tag( (protected_branch.push_access_level.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container',
- data: { field_name: "allowed_to_push_#{protected_branch.id}" }})
+
+ = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
+
- if can_admin_project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
new file mode 100644
index 00000000000..d6044aacaec
--- /dev/null
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -0,0 +1,10 @@
+%td
+ = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
+ = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
+ options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container',
+ data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
+%td
+ = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
+ = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
+ options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container',
+ data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 0b7fa8c7d06..c957cd84479 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -45,7 +45,7 @@
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c30bdb0ae91..210b43c7e0b 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -2,7 +2,22 @@
.form-group
= f.label :title, class: 'control-label'
- .col-sm-10
+
+ - issuable_template_names = issuable_templates(issuable)
+
+ - if issuable_template_names.any?
+ .col-sm-3.col-lg-2
+ .js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } }
+ - title = selected_template(issuable) || "Choose a template"
+
+ = dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
+ title: title, filter: true, placeholder: 'Filter', footer_content: true,
+ data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do
+ %ul.dropdown-footer-list
+ %li
+ %a.reset-template
+ Reset template
+ %div{ class: issuable_template_names.any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad', required: true
@@ -23,6 +38,13 @@
to prevent a
%strong Work In Progress
merge request from being merged before it's ready.
+
+ - if can_add_template?(issuable)
+ %p.help-block
+ Add
+ = link_to "issuable templates", help_page_path('workflow/description_templates')
+ to help your contributors communicate effectively!
+
.form-group.detail-page-description
= f.label :description, 'Description', class: 'control-label'
.col-sm-10
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 470dac6d75b..d2ec6c3ddef 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -29,49 +29,56 @@
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
- This url will be triggered by a push to the repository
+ This URL will be triggered by a push to the repository
%li
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
- This url will be triggered when a new tag is pushed to the repository
+ This URL will be triggered when a new tag is pushed to the repository
%li
= f.check_box :note_events, class: 'pull-left'
.prepend-left-20
= f.label :note_events, class: 'list-label' do
%strong Comments
%p.light
- This url will be triggered when someone adds a comment
+ This URL will be triggered when someone adds a comment
%li
= f.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created/updated/merged
+ This URL will be triggered when an issue is created/updated/merged
%li
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created/updated/merged
+ This URL will be triggered when a merge request is created/updated/merged
%li
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
%strong Build events
%p.light
- This url will be triggered when the build status changes
+ This URL will be triggered when the build status changes
+ %li
+ = f.check_box :pipeline_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :pipeline_events, class: 'list-label' do
+ %strong Pipeline events
+ %p.light
+ This URL will be triggered when the pipeline status changes
%li
= f.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20
= f.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events
%p.light
- This url will be triggered when a wiki page is created/updated
+ This URL will be triggered when a wiki page is created/updated
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
.checkbox
diff --git a/config/routes.rb b/config/routes.rb
index da10c5609f6..055a51514f8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -252,7 +252,11 @@ Rails.application.routes.draw do
resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy]
- resources :spam_logs, only: [:index, :destroy]
+ resources :spam_logs, only: [:index, :destroy] do
+ member do
+ post :mark_as_ham
+ end
+ end
resources :applications
@@ -524,6 +528,11 @@ Rails.application.routes.draw do
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+ #
+ # Templates
+ #
+ get '/templates/:template_type/:key' => 'templates#show', as: :template
+
scope do
get(
'/blob/*id/diff',
@@ -822,6 +831,7 @@ Rails.application.routes.draw do
member do
post :toggle_subscription
post :toggle_award_emoji
+ post :mark_as_spam
get :referenced_merge_requests
get :related_branches
get :can_create_branch
@@ -878,7 +888,10 @@ Rails.application.routes.draw do
resources :badges, only: [:index] do
collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
- get :build, constraints: { format: /svg/ }
+ constraints format: /svg/ do
+ get :build
+ get :coverage
+ end
end
end
end
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
index 6cc18bf51ed..c83502fa1d2 100644
--- a/db/fixtures/development/14_builds.rb
+++ b/db/fixtures/development/14_builds.rb
@@ -1,5 +1,21 @@
class Gitlab::Seeder::Builds
STAGES = %w[build notify_build test notify_test deploy notify_deploy]
+ BUILDS = [
+ { name: 'build:linux', stage: 'build', status: :success },
+ { name: 'build:osx', stage: 'build', status: :success },
+ { name: 'slack post build', stage: 'notify_build', status: :success },
+ { name: 'rspec:linux', stage: 'test', status: :success },
+ { name: 'rspec:windows', stage: 'test', status: :success },
+ { name: 'rspec:windows', stage: 'test', status: :success },
+ { name: 'rspec:osx', stage: 'test', status_event: :success },
+ { name: 'spinach:linux', stage: 'test', status: :pending },
+ { name: 'spinach:osx', stage: 'test', status: :canceled },
+ { name: 'cucumber:linux', stage: 'test', status: :running },
+ { name: 'cucumber:osx', stage: 'test', status: :failed },
+ { name: 'slack post test', stage: 'notify_test', status: :success },
+ { name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
+ { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :success },
+ ]
def initialize(project)
@project = project
@@ -8,25 +24,7 @@ class Gitlab::Seeder::Builds
def seed!
pipelines.each do |pipeline|
begin
- build_create!(pipeline, name: 'build:linux', stage: 'build', status_event: :success)
- build_create!(pipeline, name: 'build:osx', stage: 'build', status_event: :success)
-
- build_create!(pipeline, name: 'slack post build', stage: 'notify_build', status_event: :success)
-
- build_create!(pipeline, name: 'rspec:linux', stage: 'test', status_event: :success)
- build_create!(pipeline, name: 'rspec:windows', stage: 'test', status_event: :success)
- build_create!(pipeline, name: 'rspec:windows', stage: 'test', status_event: :success)
- build_create!(pipeline, name: 'rspec:osx', stage: 'test', status_event: :success)
- build_create!(pipeline, name: 'spinach:linux', stage: 'test', status: :pending)
- build_create!(pipeline, name: 'spinach:osx', stage: 'test', status_event: :cancel)
- build_create!(pipeline, name: 'cucumber:linux', stage: 'test', status_event: :run)
- build_create!(pipeline, name: 'cucumber:osx', stage: 'test', status_event: :drop)
-
- build_create!(pipeline, name: 'slack post test', stage: 'notify_test', status_event: :success)
-
- build_create!(pipeline, name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success)
- build_create!(pipeline, name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :success)
-
+ BUILDS.each { |opts| build_create!(pipeline, opts) }
commit_status_create!(pipeline, name: 'jenkins', status: :success)
print '.'
@@ -48,21 +46,22 @@ class Gitlab::Seeder::Builds
def build_create!(pipeline, opts = {})
attributes = build_attributes_for(pipeline, opts)
- build = Ci::Build.create!(attributes)
- if opts[:name].start_with?('build')
- artifacts_cache_file(artifacts_archive_path) do |file|
- build.artifacts_file = file
- end
+ Ci::Build.create!(attributes) do |build|
+ if opts[:name].start_with?('build')
+ artifacts_cache_file(artifacts_archive_path) do |file|
+ build.artifacts_file = file
+ end
- artifacts_cache_file(artifacts_metadata_path) do |file|
- build.artifacts_metadata = file
+ artifacts_cache_file(artifacts_metadata_path) do |file|
+ build.artifacts_metadata = file
+ end
end
- end
- if %w(running success failed).include?(build.status)
- # We need to set build trace after saving a build (id required)
- build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
+ if %w(running success failed).include?(build.status)
+ # We need to set build trace after saving a build (id required)
+ build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
+ end
end
end
diff --git a/db/migrate/20160727163552_create_user_agent_details.rb b/db/migrate/20160727163552_create_user_agent_details.rb
new file mode 100644
index 00000000000..ed4ccfedc0a
--- /dev/null
+++ b/db/migrate/20160727163552_create_user_agent_details.rb
@@ -0,0 +1,18 @@
+class CreateUserAgentDetails < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :user_agent_details do |t|
+ t.string :user_agent, null: false
+ t.string :ip_address, null: false
+ t.integer :subject_id, null: false
+ t.string :subject_type, null: false
+ t.boolean :submitted, default: false, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb b/db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb
new file mode 100644
index 00000000000..b800e6d7283
--- /dev/null
+++ b/db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb
@@ -0,0 +1,16 @@
+class AddPipelineEventsToWebHooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:web_hooks, :pipeline_events, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:web_hooks, :pipeline_events)
+ end
+end
diff --git a/db/migrate/20160728103734_add_pipeline_events_to_services.rb b/db/migrate/20160728103734_add_pipeline_events_to_services.rb
new file mode 100644
index 00000000000..bcd24fe1566
--- /dev/null
+++ b/db/migrate/20160728103734_add_pipeline_events_to_services.rb
@@ -0,0 +1,16 @@
+class AddPipelineEventsToServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:services, :pipeline_events, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:services, :pipeline_events)
+ end
+end
diff --git a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
new file mode 100644
index 00000000000..e28ab31d629
--- /dev/null
+++ b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveProjectIdFromSpamLogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Removing a column that contains data that is not used anywhere.'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ remove_column :spam_logs, :project_id, :integer
+ end
+end
diff --git a/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
new file mode 100644
index 00000000000..296f1dfac7b
--- /dev/null
+++ b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ disable_ddl_transaction!
+
+ def change
+ add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f008a6bd7a7..445f484a8c7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -897,6 +897,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.string "category", default: "common", null: false
t.boolean "default", default: false
t.boolean "wiki_page_events", default: true
+ t.boolean "pipeline_events", default: false, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
@@ -926,12 +927,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.string "source_ip"
t.string "user_agent"
t.boolean "via_api"
- t.integer "project_id"
t.string "noteable_type"
t.string "title"
t.text "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "submitted_as_ham", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
@@ -999,6 +1000,16 @@ ActiveRecord::Schema.define(version: 20160810142633) do
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
+ create_table "user_agent_details", force: :cascade do |t|
+ t.string "user_agent", null: false
+ t.string "ip_address", null: false
+ t.integer "subject_id", null: false
+ t.string "subject_type", null: false
+ t.boolean "submitted", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -1100,6 +1111,7 @@ ActiveRecord::Schema.define(version: 20160810142633) do
t.boolean "build_events", default: false, null: false
t.boolean "wiki_page_events", default: false, null: false
t.string "token"
+ t.boolean "pipeline_events", default: false, null: false
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 48a9f994759..d90d7aca4fd 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -32,6 +32,41 @@ project.
Clicking on a pipeline will show the builds that were run for that pipeline.
+## Badges
+
+There are build status and test coverage report badges available.
+
+Go to pipeline settings to see available badges and code you can use to embed
+badges in the `README.md` or your website.
+
+### Build status badge
+
+You can access a build status badge image using following link:
+
+```
+http://example.gitlab.com/namespace/project/badges/branch/build.svg
+```
+
+### Test coverage report badge
+
+GitLab makes it possible to define the regular expression for coverage report,
+that each build log will be matched against. This means that each build in the
+pipeline can have the test coverage percentage value defined.
+
+You can access test coverage badge using following link:
+
+```
+http://example.gitlab.com/namespace/project/badges/branch/coverage.svg
+```
+
+If you would like to get the coverage report from the specific job, you can add
+a `job=coverage_job_name` parameter to the URL. For example, it is possible to
+use following Markdown code to embed the est coverage report into `README.md`:
+
+```markdown
+![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
+```
+
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 6a3c416d995..c835ebc2d44 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -218,21 +218,13 @@ project's settings.
For more information read the
[Builds emails service documentation](../../project_services/builds_emails.md).
-## Builds badge
-
-You can access a builds badge image using following link:
-
-```
-http://example.gitlab.com/namespace/project/badges/branch/build.svg
-```
-
-Awesome! You started using CI in GitLab!
-
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
+Awesome! You started using CI in GitLab!
+
[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md
diff --git a/doc/development/README.md b/doc/development/README.md
index bf67b5d8dff..57f37da6f80 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -30,7 +30,11 @@
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
+
+## Databases
+
- [What requires downtime?](what_requires_downtime.md)
+- [Adding database indexes](adding_database_indexes.md)
## Compliance
diff --git a/doc/development/adding_database_indexes.md b/doc/development/adding_database_indexes.md
new file mode 100644
index 00000000000..ea6f14da3b9
--- /dev/null
+++ b/doc/development/adding_database_indexes.md
@@ -0,0 +1,123 @@
+# Adding Database Indexes
+
+Indexes can be used to speed up database queries, but when should you add a new
+index? Traditionally the answer to this question has been to add an index for
+every column used for filtering or joining data. For example, consider the
+following query:
+
+```sql
+SELECT *
+FROM projects
+WHERE user_id = 2;
+```
+
+Here we are filtering by the `user_id` column and as such a developer may decide
+to index this column.
+
+While in certain cases indexing columns using the above approach may make sense
+it can actually have a negative impact. Whenever you write data to a table any
+existing indexes need to be updated. The more indexes there are the slower this
+can potentially become. Indexes can also take up quite some disk space depending
+on the amount of data indexed and the index type. For example, PostgreSQL offers
+"GIN" indexes which can be used to index certain data types that can not be
+indexed by regular btree indexes. These indexes however generally take up more
+data and are slower to update compared to btree indexes.
+
+Because of all this one should not blindly add a new index for every column used
+to filter data by. Instead one should ask themselves the following questions:
+
+1. Can I write my query in such a way that it re-uses as many existing indexes
+ as possible?
+2. Is the data going to be large enough that using an index will actually be
+ faster than just iterating over the rows in the table?
+3. Is the overhead of maintaining the index worth the reduction in query
+ timings?
+
+We'll explore every question in detail below.
+
+## Re-using Queries
+
+The first step is to make sure your query re-uses as many existing indexes as
+possible. For example, consider the following query:
+
+```sql
+SELECT *
+FROM todos
+WHERE user_id = 123
+AND state = 'open';
+```
+
+Now imagine we already have an index on the `user_id` column but not on the
+`state` column. One may think this query will perform badly due to `state` being
+unindexed. In reality the query may perform just fine given the index on
+`user_id` can filter out enough rows.
+
+The best way to determine if indexes are re-used is to run your query using
+`EXPLAIN ANALYZE`. Depending on any extra tables that may be joined and
+other columns being used for filtering you may find an extra index is not going
+to make much (if any) difference. On the other hand you may determine that the
+index _may_ make a difference.
+
+In short:
+
+1. Try to write your query in such a way that it re-uses as many existing
+ indexes as possible.
+2. Run the query using `EXPLAIN ANALYZE` and study the output to find the most
+ ideal query.
+
+## Data Size
+
+A database may decide not to use an index despite it existing in case a regular
+sequence scan (= simply iterating over all existing rows) is faster. This is
+especially the case for small tables.
+
+If a table is expected to grow in size and you expect your query has to filter
+out a lot of rows you may want to consider adding an index. If the table size is
+very small (e.g. only a handful of rows) or any existing indexes filter out
+enough rows you may _not_ want to add a new index.
+
+## Maintenance Overhead
+
+Indexes have to be updated on every table write. In case of PostgreSQL _all_
+existing indexes will be updated whenever data is written to a table. As a
+result of this having many indexes on the same table will slow down writes.
+
+Because of this one should ask themselves: is the reduction in query performance
+worth the overhead of maintaining an extra index?
+
+If adding an index reduces SELECT timings by 5 milliseconds but increases
+INSERT/UPDATE/DELETE timings by 10 milliseconds then the index may not be worth
+it. On the other hand, if SELECT timings are reduced but INSERT/UPDATE/DELETE
+timings are not affected you may want to add the index after all.
+
+## Finding Unused Indexes
+
+To see which indexes are unused you can run the following query:
+
+```sql
+SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
+FROM pg_stat_all_indexes
+WHERE schemaname = 'public'
+AND idx_scan = 0
+AND idx_tup_read = 0
+AND idx_tup_fetch = 0
+ORDER BY pg_relation_size(indexrelname::regclass) desc;
+```
+
+This query outputs a list containing all indexes that are never used and sorts
+them by indexes sizes in descending order. This query can be useful to
+determine if any previously indexes are useful after all. More information on
+the meaning of the various columns can be found at
+<https://www.postgresql.org/docs/current/static/monitoring-stats.html>.
+
+Because the output of this query relies on the actual usage of your database it
+may be affected by factors such as (but not limited to):
+
+* Certain queries never being executed, thus not being able to use certain
+ indexes.
+* Certain tables having little data, resulting in PostgreSQL using sequence
+ scans instead of index scans.
+
+In other words, this data is only reliable for a frequently used database with
+plenty of data and with as many GitLab features enabled (and being used) as
+possible.
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index 3a8c823e026..2d1d504202c 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -15,11 +15,14 @@ repository and maintained by GitLab UX designers.
## Navigation
GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu.
-This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo
-and the current user's profile picture. The content section contains a header and the content itself.
-The header describes the current GitLab page and what navigation is
-available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the
-project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group.
+This menu will be visible regardless of what page you visit.
+The content section contains a header and the content itself. The header describes the current GitLab page and what navigation is
+available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
+project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
+
+You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
+along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
+
### Adding new tab to header navigation
@@ -99,3 +102,6 @@ Do not use both green and blue button in one form.
display counts in the UI.
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
+[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
+[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
+[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md
index c222d21612f..a6436b5f926 100644
--- a/doc/integration/akismet.md
+++ b/doc/integration/akismet.md
@@ -22,14 +22,37 @@ To use Akismet:
2. Sign-in or create a new account.
-3. Click on "Show" to reveal the API key.
+3. Click on **Show** to reveal the API key.
4. Go to Applications Settings on Admin Area (`admin/application_settings`)
-5. Check the `Enable Akismet` checkbox
+5. Check the **Enable Akismet** checkbox
6. Fill in the API key from step 3.
7. Save the configuration.
![Screenshot of Akismet settings](img/akismet_settings.png)
+
+
+## Training
+
+> *Note:* Training the Akismet filter is only available in 8.11 and above.
+
+As a way to better recognize between spam and ham, you can train the Akismet
+filter whenever there is a false positive or false negative.
+
+When an entry is recognized as spam, it is rejected and added to the Spam Logs.
+From here you can review if they are really spam. If one of them is not really
+spam, you can use the **Submit as ham** button to tell Akismet that it falsely
+recognized an entry as spam.
+
+![Screenshot of Spam Logs](img/spam_log.png)
+
+If an entry that is actually spam was not recognized as such, you will be able
+to also submit this to Akismet. The **Submit as spam** button will only appear
+to admin users.
+
+![Screenshot of Issue](img/submit_issue.png)
+
+Training Akismet will help it to recognize spam more accurately in the future.
diff --git a/doc/integration/img/spam_log.png b/doc/integration/img/spam_log.png
new file mode 100644
index 00000000000..8d574448690
--- /dev/null
+++ b/doc/integration/img/spam_log.png
Binary files differ
diff --git a/doc/integration/img/submit_issue.png b/doc/integration/img/submit_issue.png
new file mode 100644
index 00000000000..5c7896a7eec
--- /dev/null
+++ b/doc/integration/img/submit_issue.png
Binary files differ
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 2513def49a4..08ff89ce6ae 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -7,8 +7,7 @@
> than that of the exporter.
> - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'.
-> Ask your administrator if you don't see the **GitLab export** button when
-> creating a new project.
+> You will have to be an administrator to enable and use the import functionality.
> - You can find some useful raketasks if you are an administrator in the
> [import_export](../../../administration/raketasks/project_import_export.md)
> raketask.
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index d4b28d875cd..33c1a79d59c 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -754,6 +754,174 @@ X-Gitlab-Event: Wiki Page Hook
}
```
+## Pipeline events
+
+Triggered on status change of Pipeline.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Pipeline Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "pipeline",
+ "object_attributes":{
+ "id": 31,
+ "ref": "master",
+ "tag": false,
+ "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "status": "success",
+ "stages":[
+ "build",
+ "test",
+ "deploy"
+ ],
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "duration": 63
+ },
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "project":{
+ "name": "Gitlab Test",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "avatar_url": null,
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "namespace": "Gitlab Org",
+ "visibility_level": 20,
+ "path_with_namespace": "gitlab-org/gitlab-test",
+ "default_branch": "master"
+ },
+ "commit":{
+ "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "message": "test\n",
+ "timestamp": "2016-08-12T17:23:21+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "author":{
+ "name": "User",
+ "email": "user@gitlab.com"
+ }
+ },
+ "builds":[
+ {
+ "id": 380,
+ "stage": "deploy",
+ "name": "production",
+ "status": "skipped",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "manual",
+ "manual": true,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 377,
+ "stage": "test",
+ "name": "test-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 378,
+ "stage": "test",
+ "name": "test-build",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 376,
+ "stage": "build",
+ "name": "build-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:24:56 UTC",
+ "finished_at": "2016-08-12 15:25:26 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 379,
+ "stage": "deploy",
+ "name": "staging",
+ "status": "created",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ }
+ ]
+}
+```
+
#### Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 49dec613716..993349e5b46 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -17,6 +17,7 @@
- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
- [Releases](releases.md)
+- [Issuable Templates](issuable_templates.md)
- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- [Revert changes](revert_changes.md)
diff --git a/doc/workflow/description_templates.md b/doc/workflow/description_templates.md
new file mode 100644
index 00000000000..9514564af02
--- /dev/null
+++ b/doc/workflow/description_templates.md
@@ -0,0 +1,12 @@
+# Description templates
+
+Description templates allow you to define context-specific templates for issue and merge request description fields for your project. When in use, users that create a new issue or merge request can select a description template to help them communicate with other contributors effectively.
+
+Every GitLab project can define its own set of description templates as they are added to the root directory of a GitLab project's repository.
+
+Description templates are written in markdown _(`.md`)_ and stored in your projects repository under the `/.gitlab/issue_templates/` and `/.gitlab/merge_request_templates/` directories.
+
+![Description templates](img/description_templates.png)
+
+_Example:_
+`/.gitlab/issue_templates/bug.md` will enable the `bug` dropdown option for new issues. When `bug` is selected, the content from the `bug.md` template file will be copied to the issue description field.
diff --git a/doc/workflow/img/description_templates.png b/doc/workflow/img/description_templates.png
new file mode 100644
index 00000000000..af2e9403826
--- /dev/null
+++ b/doc/workflow/img/description_templates.png
Binary files differ
diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature
index 8ddafb6a7ac..046e2815d4e 100644
--- a/features/dashboard/new_project.feature
+++ b/features/dashboard/new_project.feature
@@ -9,7 +9,7 @@ Background:
@javascript
Scenario: I should see New Projects page
Then I see "New Project" page
- Then I see all possible import optios
+ Then I see all possible import options
@javascript
Scenario: I should see instructions on how to import from Git URL
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index dcfa88f69fc..f0d8d498e46 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -14,14 +14,13 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_content('Project name')
end
- step 'I see all possible import optios' do
+ step 'I see all possible import options' do
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
expect(page).to have_link('Gitorious.org')
expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL')
- expect(page).to have_link('GitLab export')
end
step 'I click on "Import project from GitHub"' do
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index a77afe634f6..b615703df93 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -61,22 +61,27 @@ module API
name: @branch.name
}
- unless developers_can_merge.nil?
- protected_branch_params.merge!({
- merge_access_level_attributes: {
- access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }
- })
+ # If `developers_can_merge` is switched off, _all_ `DEVELOPER`
+ # merge_access_levels need to be deleted.
+ if developers_can_merge == false
+ protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
end
- unless developers_can_push.nil?
- protected_branch_params.merge!({
- push_access_level_attributes: {
- access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
- }
- })
+ # If `developers_can_push` is switched off, _all_ `DEVELOPER`
+ # push_access_levels need to be deleted.
+ if developers_can_push == false
+ protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
end
+ protected_branch_params.merge!(
+ merge_access_levels_attributes: [{
+ access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }],
+ push_access_levels_attributes: [{
+ access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }]
+ )
+
if protected_branch
service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
service.execute(protected_branch)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index ae74d14a4bb..055716ab1e3 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -48,7 +48,8 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
- expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
+ expose :issues_events, :merge_requests_events, :tag_push_events
+ expose :note_events, :build_events, :pipeline_events
expose :enable_ssl_verification
end
@@ -129,12 +130,14 @@ module API
expose :developers_can_push do |repo_branch, options|
project = options[:project]
- project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.push_access_level.access_level == Gitlab::Access::DEVELOPER }
+ access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten
+ access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end
expose :developers_can_merge do |repo_branch, options|
project = options[:project]
- project.protected_branches.matching(repo_branch.name).any? { |protected_branch| protected_branch.merge_access_level.access_level == Gitlab::Access::DEVELOPER }
+ access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten
+ access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end
end
@@ -344,7 +347,8 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
+ expose :push_events, :issues_events, :merge_requests_events
+ expose :tag_push_events, :note_events, :build_events, :pipeline_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c4d3134da6c..077258faee1 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -3,8 +3,6 @@ module API
class Issues < Grape::API
before { authenticate! }
- helpers ::Gitlab::AkismetHelper
-
helpers do
def filter_issues_state(issues, state)
case state
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 6bb70bc8bc3..3f63cd678e8 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -45,6 +45,7 @@ module API
:tag_push_events,
:note_events,
:build_events,
+ :pipeline_events,
:enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
@@ -78,6 +79,7 @@ module API
:tag_push_events,
:note_events,
:build_events,
+ :pipeline_events,
:enable_ssl_verification
]
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 18408797756..b9e718147e1 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,21 +1,28 @@
module API
class Templates < Grape::API
- TEMPLATE_TYPES = {
- gitignores: Gitlab::Template::Gitignore,
- gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
+ GLOBAL_TEMPLATE_TYPES = {
+ gitignores: Gitlab::Template::GitignoreTemplate,
+ gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
}.freeze
- TEMPLATE_TYPES.each do |template, klass|
+ helpers do
+ def render_response(template_type, template)
+ not_found!(template_type.to_s.singularize) unless template
+ present template, with: Entities::Template
+ end
+ end
+
+ GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
# Get the list of the available template
#
# Example Request:
# GET /gitignores
# GET /gitlab_ci_ymls
- get template.to_s do
+ get template_type.to_s do
present klass.all, with: Entities::TemplatesList
end
- # Get the text for a specific template
+ # Get the text for a specific template present in local filesystem
#
# Parameters:
# name (required) - The name of a template
@@ -23,13 +30,10 @@ module API
# Example Request:
# GET /gitignores/Elixir
# GET /gitlab_ci_ymls/Ruby
- get "#{template}/:name" do
+ get "#{template_type}/:name" do
required_attributes! [:name]
-
new_template = klass.find(params[:name])
- not_found!(template.to_s.singularize) unless new_template
-
- present new_template, with: Entities::Template
+ render_response(template_type, new_template)
end
end
end
diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb
deleted file mode 100644
index 207736b59db..00000000000
--- a/lib/gitlab/akismet_helper.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Gitlab
- module AkismetHelper
- def akismet_enabled?
- current_application_settings.akismet_enabled
- end
-
- def akismet_client
- @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
- Gitlab.config.gitlab.url)
- end
-
- def client_ip(env)
- env['action_dispatch.remote_ip'].to_s
- end
-
- def user_agent(env)
- env['HTTP_USER_AGENT']
- end
-
- def check_for_spam?(project)
- akismet_enabled? && project.public?
- end
-
- def is_spam?(environment, user, text)
- client = akismet_client
- ip_address = client_ip(environment)
- user_agent = user_agent(environment)
-
- params = {
- type: 'comment',
- text: text,
- created_at: DateTime.now,
- author: user.name,
- author_email: user.email,
- referrer: environment['HTTP_REFERER'],
- }
-
- begin
- is_spam, is_blatant = client.check(ip_address, user_agent, params)
- is_spam || is_blatant
- rescue => e
- Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/badge/base.rb b/lib/gitlab/badge/base.rb
new file mode 100644
index 00000000000..909fa24fa90
--- /dev/null
+++ b/lib/gitlab/badge/base.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Badge
+ class Base
+ def entity
+ raise NotImplementedError
+ end
+
+ def status
+ raise NotImplementedError
+ end
+
+ def metadata
+ raise NotImplementedError
+ end
+
+ def template
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb
deleted file mode 100644
index 1de721a2269..00000000000
--- a/lib/gitlab/badge/build.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Gitlab
- module Badge
- ##
- # Build badge
- #
- class Build
- delegate :key_text, :value_text, to: :template
-
- def initialize(project, ref)
- @project = project
- @ref = ref
- @sha = @project.commit(@ref).try(:sha)
- end
-
- def status
- @project.pipelines
- .where(sha: @sha, ref: @ref)
- .status || 'unknown'
- end
-
- def metadata
- @metadata ||= Build::Metadata.new(@project, @ref)
- end
-
- def template
- @template ||= Build::Template.new(status)
- end
- end
- end
-end
diff --git a/lib/gitlab/badge/build/metadata.rb b/lib/gitlab/badge/build/metadata.rb
index 553ef8d7b16..f87a7b7942e 100644
--- a/lib/gitlab/badge/build/metadata.rb
+++ b/lib/gitlab/badge/build/metadata.rb
@@ -1,25 +1,17 @@
module Gitlab
module Badge
- class Build
+ module Build
##
# Class that describes build badge metadata
#
- class Metadata
- include Gitlab::Application.routes.url_helpers
- include ActionView::Helpers::AssetTagHelper
- include ActionView::Helpers::UrlHelper
-
- def initialize(project, ref)
- @project = project
- @ref = ref
- end
-
- def to_html
- link_to(image_tag(image_url, alt: 'build status'), link_url)
+ class Metadata < Badge::Metadata
+ def initialize(badge)
+ @project = badge.project
+ @ref = badge.ref
end
- def to_markdown
- "[![build status](#{image_url})](#{link_url})"
+ def title
+ 'build status'
end
def image_url
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
new file mode 100644
index 00000000000..50aa45e5406
--- /dev/null
+++ b/lib/gitlab/badge/build/status.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Badge
+ module Build
+ ##
+ # Build status badge
+ #
+ class Status < Badge::Base
+ attr_reader :project, :ref
+
+ def initialize(project, ref)
+ @project = project
+ @ref = ref
+
+ @sha = @project.commit(@ref).try(:sha)
+ end
+
+ def entity
+ 'build'
+ end
+
+ def status
+ @project.pipelines
+ .where(sha: @sha, ref: @ref)
+ .status || 'unknown'
+ end
+
+ def metadata
+ @metadata ||= Build::Metadata.new(self)
+ end
+
+ def template
+ @template ||= Build::Template.new(self)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
index deba3b669b3..2b95ddfcb53 100644
--- a/lib/gitlab/badge/build/template.rb
+++ b/lib/gitlab/badge/build/template.rb
@@ -1,12 +1,12 @@
module Gitlab
module Badge
- class Build
+ module Build
##
# Class that represents a build badge template.
#
# Template object will be passed to badge.svg.erb template.
#
- class Template
+ class Template < Badge::Template
STATUS_COLOR = {
success: '#4c1',
failed: '#e05d44',
@@ -17,16 +17,17 @@ module Gitlab
unknown: '#9f9f9f'
}
- def initialize(status)
- @status = status
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
end
def key_text
- 'build'
+ @entity.to_s
end
def value_text
- @status
+ @status.to_s
end
def key_width
@@ -37,25 +38,8 @@ module Gitlab
54
end
- def key_color
- '#555'
- end
-
def value_color
- STATUS_COLOR[@status.to_sym] ||
- STATUS_COLOR[:unknown]
- end
-
- def key_text_anchor
- key_width / 2
- end
-
- def value_text_anchor
- key_width + (value_width / 2)
- end
-
- def width
- key_width + value_width
+ STATUS_COLOR[@status.to_sym] || STATUS_COLOR[:unknown]
end
end
end
diff --git a/lib/gitlab/badge/coverage/metadata.rb b/lib/gitlab/badge/coverage/metadata.rb
new file mode 100644
index 00000000000..53588185622
--- /dev/null
+++ b/lib/gitlab/badge/coverage/metadata.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Class that describes coverage badge metadata
+ #
+ class Metadata < Badge::Metadata
+ def initialize(badge)
+ @project = badge.project
+ @ref = badge.ref
+ @job = badge.job
+ end
+
+ def title
+ 'coverage report'
+ end
+
+ def image_url
+ coverage_namespace_project_badges_url(@project.namespace,
+ @project, @ref,
+ format: :svg)
+ end
+
+ def link_url
+ namespace_project_commits_url(@project.namespace, @project, id: @ref)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
new file mode 100644
index 00000000000..3d56ea3e47a
--- /dev/null
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -0,0 +1,56 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Test coverage report badge
+ #
+ class Report < Badge::Base
+ attr_reader :project, :ref, :job
+
+ def initialize(project, ref, job = nil)
+ @project = project
+ @ref = ref
+ @job = job
+
+ @pipeline = @project.pipelines
+ .where(ref: @ref)
+ .where(sha: @project.commit(@ref).try(:sha))
+ .first
+ end
+
+ def entity
+ 'coverage'
+ end
+
+ def status
+ @coverage ||= raw_coverage
+ return unless @coverage
+
+ @coverage.to_i
+ end
+
+ def metadata
+ @metadata ||= Coverage::Metadata.new(self)
+ end
+
+ def template
+ @template ||= Coverage::Template.new(self)
+ end
+
+ private
+
+ def raw_coverage
+ return unless @pipeline
+
+ if @job.blank?
+ @pipeline.coverage
+ else
+ @pipeline.builds
+ .find_by(name: @job)
+ .try(:coverage)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
new file mode 100644
index 00000000000..06e0d084e9f
--- /dev/null
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Class that represents a coverage badge template.
+ #
+ # Template object will be passed to badge.svg.erb template.
+ #
+ class Template < Badge::Template
+ STATUS_COLOR = {
+ good: '#4c1',
+ acceptable: '#a3c51c',
+ medium: '#dfb317',
+ low: '#e05d44',
+ unknown: '#9f9f9f'
+ }
+
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
+ end
+
+ def key_text
+ @entity.to_s
+ end
+
+ def value_text
+ @status ? "#{@status}%" : 'unknown'
+ end
+
+ def key_width
+ 62
+ end
+
+ def value_width
+ @status ? 36 : 58
+ end
+
+ def value_color
+ case @status
+ when 95..100 then STATUS_COLOR[:good]
+ when 90..95 then STATUS_COLOR[:acceptable]
+ when 75..90 then STATUS_COLOR[:medium]
+ when 0..75 then STATUS_COLOR[:low]
+ else
+ STATUS_COLOR[:unknown]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/metadata.rb b/lib/gitlab/badge/metadata.rb
new file mode 100644
index 00000000000..548f85b78bb
--- /dev/null
+++ b/lib/gitlab/badge/metadata.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Badge
+ ##
+ # Abstract class for badge metadata
+ #
+ class Metadata
+ include Gitlab::Application.routes.url_helpers
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::UrlHelper
+
+ def initialize(badge)
+ @badge = badge
+ end
+
+ def to_html
+ link_to(image_tag(image_url, alt: title), link_url)
+ end
+
+ def to_markdown
+ "[![#{title}](#{image_url})](#{link_url})"
+ end
+
+ def title
+ raise NotImplementedError
+ end
+
+ def image_url
+ raise NotImplementedError
+ end
+
+ def link_url
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/template.rb b/lib/gitlab/badge/template.rb
new file mode 100644
index 00000000000..bfeb0052642
--- /dev/null
+++ b/lib/gitlab/badge/template.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Badge
+ ##
+ # Abstract template class for badges
+ #
+ class Template
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
+ end
+
+ def key_text
+ raise NotImplementedError
+ end
+
+ def value_text
+ raise NotImplementedError
+ end
+
+ def key_width
+ raise NotImplementedError
+ end
+
+ def value_width
+ raise NotImplementedError
+ end
+
+ def value_color
+ raise NotImplementedError
+ end
+
+ def key_color
+ '#555'
+ end
+
+ def key_text_anchor
+ key_width / 2
+ end
+
+ def value_text_anchor
+ key_width + (value_width / 2)
+ end
+
+ def width
+ key_width + value_width
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/data_builder/build.rb
index 9f45aefda0f..6548e6475c6 100644
--- a/lib/gitlab/build_data_builder.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -1,6 +1,8 @@
module Gitlab
- class BuildDataBuilder
- class << self
+ module DataBuilder
+ module Build
+ extend self
+
def build(build)
project = build.project
commit = build.pipeline
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/data_builder/note.rb
index 8bdc89a7751..50fea1232af 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/data_builder/note.rb
@@ -1,6 +1,8 @@
module Gitlab
- class NoteDataBuilder
- class << self
+ module DataBuilder
+ module Note
+ extend self
+
# Produce a hash of post-receive data
#
# For all notes:
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
new file mode 100644
index 00000000000..06a783ebc1c
--- /dev/null
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module DataBuilder
+ module Pipeline
+ extend self
+
+ def build(pipeline)
+ {
+ object_kind: 'pipeline',
+ object_attributes: hook_attrs(pipeline),
+ user: pipeline.user.try(:hook_attrs),
+ project: pipeline.project.hook_attrs(backward: false),
+ commit: pipeline.commit.try(:hook_attrs),
+ builds: pipeline.builds.map(&method(:build_hook_attrs))
+ }
+ end
+
+ def hook_attrs(pipeline)
+ {
+ id: pipeline.id,
+ ref: pipeline.ref,
+ tag: pipeline.tag,
+ sha: pipeline.sha,
+ before_sha: pipeline.before_sha,
+ status: pipeline.status,
+ stages: pipeline.stages,
+ created_at: pipeline.created_at,
+ finished_at: pipeline.finished_at,
+ duration: pipeline.duration
+ }
+ end
+
+ def build_hook_attrs(build)
+ {
+ id: build.id,
+ stage: build.stage,
+ name: build.name,
+ status: build.status,
+ created_at: build.created_at,
+ started_at: build.started_at,
+ finished_at: build.finished_at,
+ when: build.when,
+ manual: build.manual?,
+ user: build.user.try(:hook_attrs),
+ runner: build.runner && runner_hook_attrs(build.runner),
+ artifacts_file: {
+ filename: build.artifacts_file.filename,
+ size: build.artifacts_size
+ }
+ }
+ end
+
+ def runner_hook_attrs(runner)
+ {
+ id: runner.id,
+ description: runner.description,
+ active: runner.active?,
+ is_shared: runner.is_shared?
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/data_builder/push.rb
index c8f12577112..4f81863da35 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -1,6 +1,8 @@
module Gitlab
- class PushDataBuilder
- class << self
+ module DataBuilder
+ module Push
+ extend self
+
# Produce a hash of post-receive data
#
# data = {
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
index 008300bde45..0cc10f40087 100644
--- a/lib/gitlab/import_export/json_hash_builder.rb
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -57,19 +57,16 @@ module Gitlab
# +value+ existing model to be included in the hash
# +json_config_hash+ the original hash containing the root model
def create_model_value(current_key, value, json_config_hash)
- parsed_hash = { include: value }
- parse_hash(value, parsed_hash)
-
- json_config_hash[current_key] = parsed_hash
+ json_config_hash[current_key] = parse_hash(value) || { include: value }
end
# Calls attributes finder to parse the hash and add any attributes to it
#
# +value+ existing model to be included in the hash
# +parsed_hash+ the original hash
- def parse_hash(value, parsed_hash)
+ def parse_hash(value)
@attributes_finder.parse(value) do |hash|
- parsed_hash = { include: hash_or_merge(value, hash) }
+ { include: hash_or_merge(value, hash) }
end
end
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
index 760ff3e614a..7ebec8e2cff 100644
--- a/lib/gitlab/template/base_template.rb
+++ b/lib/gitlab/template/base_template.rb
@@ -1,8 +1,9 @@
module Gitlab
module Template
class BaseTemplate
- def initialize(path)
+ def initialize(path, project = nil)
@path = path
+ @finder = self.class.finder(project)
end
def name
@@ -10,23 +11,32 @@ module Gitlab
end
def content
- File.read(@path)
+ @finder.read(@path)
+ end
+
+ def to_json
+ { name: name, content: content }
end
class << self
- def all
- self.categories.keys.flat_map { |cat| by_category(cat) }
+ def all(project = nil)
+ if categories.any?
+ categories.keys.flat_map { |cat| by_category(cat, project) }
+ else
+ by_category("", project)
+ end
end
- def find(key)
- file_name = "#{key}#{self.extension}"
-
- directory = select_directory(file_name)
- directory ? new(File.join(category_directory(directory), file_name)) : nil
+ def find(key, project = nil)
+ path = self.finder(project).find(key)
+ path.present? ? new(path, project) : nil
end
+ # Set categories as sub directories
+ # Example: { "category_name_1" => "directory_path_1", "category_name_2" => "directory_name_2" }
+ # Default is no category with all files in base dir of each class
def categories
- raise NotImplementedError
+ {}
end
def extension
@@ -37,29 +47,40 @@ module Gitlab
raise NotImplementedError
end
- def by_category(category)
- templates_for_directory(category_directory(category))
+ # Defines which strategy will be used to get templates files
+ # RepoTemplateFinder - Finds templates on project repository, templates are filtered perproject
+ # GlobalTemplateFinder - Finds templates on gitlab installation source, templates can be used in all projects
+ def finder(project = nil)
+ raise NotImplementedError
end
- def category_directory(category)
- File.join(base_dir, categories[category])
+ def by_category(category, project = nil)
+ directory = category_directory(category)
+ files = finder(project).list_files_for(directory)
+
+ files.map { |f| new(f, project) }
end
- private
+ def category_directory(category)
+ return base_dir unless category.present?
- def select_directory(file_name)
- categories.keys.find do |category|
- File.exist?(File.join(category_directory(category), file_name))
- end
+ File.join(base_dir, categories[category])
end
- def templates_for_directory(dir)
- dir << '/' unless dir.end_with?('/')
- Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
- end
+ # If template is organized by category it returns { category_name: [{ name: template_name }, { name: template2_name }] }
+ # If no category is present returns [{ name: template_name }, { name: template2_name}]
+ def dropdown_names(project = nil)
+ return [] if project && !project.repository.exists?
- def filter_regex
- @filter_reges ||= /#{Regexp.escape(extension)}\z/
+ if categories.any?
+ categories.keys.map do |category|
+ files = self.by_category(category, project)
+ [category, files.map { |t| { name: t.name } }]
+ end.to_h
+ else
+ files = self.all(project)
+ files.map { |t| { name: t.name } }
+ end
end
end
end
diff --git a/lib/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb
new file mode 100644
index 00000000000..473b05257c6
--- /dev/null
+++ b/lib/gitlab/template/finders/base_template_finder.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Template
+ module Finders
+ class BaseTemplateFinder
+ def initialize(base_dir)
+ @base_dir = base_dir
+ end
+
+ def list_files_for
+ raise NotImplementedError
+ end
+
+ def read
+ raise NotImplementedError
+ end
+
+ def find
+ raise NotImplementedError
+ end
+
+ def category_directory(category)
+ return @base_dir unless category.present?
+
+ @base_dir + @categories[category]
+ end
+
+ class << self
+ def filter_regex(extension)
+ /#{Regexp.escape(extension)}\z/
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb
new file mode 100644
index 00000000000..831da45191f
--- /dev/null
+++ b/lib/gitlab/template/finders/global_template_finder.rb
@@ -0,0 +1,38 @@
+# Searches and reads file present on Gitlab installation directory
+module Gitlab
+ module Template
+ module Finders
+ class GlobalTemplateFinder < BaseTemplateFinder
+ def initialize(base_dir, extension, categories = {})
+ @categories = categories
+ @extension = extension
+ super(base_dir)
+ end
+
+ def read(path)
+ File.read(path)
+ end
+
+ def find(key)
+ file_name = "#{key}#{@extension}"
+
+ directory = select_directory(file_name)
+ directory ? File.join(category_directory(directory), file_name) : nil
+ end
+
+ def list_files_for(dir)
+ dir << '/' unless dir.end_with?('/')
+ Dir.glob(File.join(dir, "*#{@extension}")).select { |f| f =~ self.class.filter_regex(@extension) }
+ end
+
+ private
+
+ def select_directory(file_name)
+ @categories.keys.find do |category|
+ File.exist?(File.join(category_directory(category), file_name))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
new file mode 100644
index 00000000000..22c39436cb2
--- /dev/null
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -0,0 +1,59 @@
+# Searches and reads files present on each Gitlab project repository
+module Gitlab
+ module Template
+ module Finders
+ class RepoTemplateFinder < BaseTemplateFinder
+ # Raised when file is not found
+ class FileNotFoundError < StandardError; end
+
+ def initialize(project, base_dir, extension, categories = {})
+ @categories = categories
+ @extension = extension
+ @repository = project.repository
+ @commit = @repository.head_commit if @repository.exists?
+
+ super(base_dir)
+ end
+
+ def read(path)
+ blob = @repository.blob_at(@commit.id, path) if @commit
+ raise FileNotFoundError if blob.nil?
+ blob.data
+ end
+
+ def find(key)
+ file_name = "#{key}#{@extension}"
+ directory = select_directory(file_name)
+ raise FileNotFoundError if directory.nil?
+
+ category_directory(directory) + file_name
+ end
+
+ def list_files_for(dir)
+ return [] unless @commit
+
+ dir << '/' unless dir.end_with?('/')
+
+ entries = @repository.tree(:head, dir).entries
+
+ names = entries.map(&:name)
+ names.select { |f| f =~ self.class.filter_regex(@extension) }
+ end
+
+ private
+
+ def select_directory(file_name)
+ return [] unless @commit
+
+ # Insert root as directory
+ directories = ["", @categories.keys]
+
+ directories.find do |category|
+ path = category_directory(category) + file_name
+ @repository.blob_at(@commit.id, path)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore_template.rb
index 964fbfd4de3..8d2a9d2305c 100644
--- a/lib/gitlab/template/gitignore.rb
+++ b/lib/gitlab/template/gitignore_template.rb
@@ -1,6 +1,6 @@
module Gitlab
module Template
- class Gitignore < BaseTemplate
+ class GitignoreTemplate < BaseTemplate
class << self
def extension
'.gitignore'
@@ -16,6 +16,10 @@ module Gitlab
def base_dir
Rails.root.join('vendor/gitignore')
end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
end
end
end
diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
index 7f480fe33c0..8d1a1ed54c9 100644
--- a/lib/gitlab/template/gitlab_ci_yml.rb
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -1,6 +1,6 @@
module Gitlab
module Template
- class GitlabCiYml < BaseTemplate
+ class GitlabCiYmlTemplate < BaseTemplate
def content
explanation = "# This file is a template, and might need editing before it works on your project."
[explanation, super].join("\n")
@@ -21,6 +21,10 @@ module Gitlab
def base_dir
Rails.root.join('vendor/gitlab-ci-yml')
end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
end
end
end
diff --git a/lib/gitlab/template/issue_template.rb b/lib/gitlab/template/issue_template.rb
new file mode 100644
index 00000000000..c6fa8d3eafc
--- /dev/null
+++ b/lib/gitlab/template/issue_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Template
+ class IssueTemplate < BaseTemplate
+ class << self
+ def extension
+ '.md'
+ end
+
+ def base_dir
+ '.gitlab/issue_templates/'
+ end
+
+ def finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/merge_request_template.rb b/lib/gitlab/template/merge_request_template.rb
new file mode 100644
index 00000000000..f826c02f3b5
--- /dev/null
+++ b/lib/gitlab/template/merge_request_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Template
+ class MergeRequestTemplate < BaseTemplate
+ class << self
+ def extension
+ '.md'
+ end
+
+ def base_dir
+ '.gitlab/merge_request_templates/'
+ end
+
+ def finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index c55a7fc4d3d..9858d2e7d83 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -32,7 +32,7 @@ module Gitlab
if project.protected_branch?(ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
- access_levels = project.protected_branches.matching(ref).map(&:push_access_level)
+ access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
access_levels.any? { |access_level| access_level.check_access(user) }
else
user.can?(:push_code, project)
@@ -43,7 +43,7 @@ module Gitlab
return false unless user
if project.protected_branch?(ref)
- access_levels = project.protected_branches.matching(ref).map(&:merge_access_level)
+ access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
access_levels.any? { |access_level| access_level.check_access(user) }
else
user.can?(:push_code, project)
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 0239aea47fb..602de72d23f 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -7,12 +7,13 @@ describe Admin::GroupsController do
before do
sign_in(admin)
- Sidekiq::Testing.fake!
end
describe 'DELETE #destroy' do
it 'schedules a group destroy' do
- expect { delete :destroy, id: project.group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ Sidekiq::Testing.fake! do
+ expect { delete :destroy, id: project.group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
end
it 'redirects to the admin group path' do
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 520a4f6f9c5..585ca31389d 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -34,4 +34,16 @@ describe Admin::SpamLogsController do
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ describe '#mark_as_ham' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:submit_ham).and_return(true)
+ end
+ it 'submits the log as ham' do
+ post :mark_as_ham, id: first_spam.id
+
+ expect(response).to have_http_status(302)
+ expect(SpamLog.find(first_spam.id).submitted_as_ham).to be_truthy
+ end
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 4ae6364207b..a763e2c5ba8 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -89,12 +89,13 @@ describe GroupsController do
context 'as the group owner' do
before do
- Sidekiq::Testing.fake!
sign_in(user)
end
it 'schedules a group destroy' do
- expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ Sidekiq::Testing.fake! do
+ expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
end
it 'redirects to the root path' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index b6a0276846c..0836b71056c 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -274,8 +274,8 @@ describe Projects::IssuesController do
describe 'POST #create' do
context 'Akismet is enabled' do
before do
- allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
def post_spam_issue
@@ -300,6 +300,52 @@ describe Projects::IssuesController do
expect(spam_logs[0].title).to eq('Spam Title')
end
end
+
+ context 'user agent details are saved' do
+ before do
+ request.env['action_dispatch.remote_ip'] = '127.0.0.1'
+ end
+
+ def post_new_issue
+ sign_in(user)
+ project = create(:empty_project, :public)
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ issue: { title: 'Title', description: 'Description' }
+ }
+ end
+
+ it 'creates a user agent detail' do
+ expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ context 'properly submits to Akismet' do
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ allow_any_instance_of(ApplicationSetting).to receive_messages(akismet_enabled: true)
+ end
+
+ def post_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: issue)
+ project.team << [admin, :master]
+ sign_in(admin)
+ post :mark_as_spam, {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: issue.iid
+ }
+ end
+
+ it 'updates issue' do
+ post_spam
+ expect(issue.submittable_as_spam?).to be_falsey
+ end
+ end
end
describe "DELETE #destroy" do
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
new file mode 100644
index 00000000000..7b3a26d7ca7
--- /dev/null
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Projects::TemplatesController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+ let(:body) { JSON.parse(response.body) }
+
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ end
+
+ describe '#show' do
+ it 'renders template name and content as json' do
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(200)
+ expect(body["name"]).to eq("bug")
+ expect(body["content"]).to eq("something valid")
+ end
+
+ it 'renders 404 when unauthorized' do
+ sign_in(user2)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'renders 404 when template type is not found' do
+ sign_in(user)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'renders 404 without errors' do
+ sign_in(user)
+ expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 3195fb3ddcc..4fd51a23490 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -5,5 +5,15 @@ FactoryGirl.define do
trait :token do
token { SecureRandom.hex(10) }
end
+
+ trait :all_events_enabled do
+ push_events true
+ merge_requests_events true
+ tag_push_events true
+ issues_events true
+ note_events true
+ build_events true
+ pipeline_events true
+ end
end
end
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index 5575852c2d7..b2695e0482a 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -3,26 +3,26 @@ FactoryGirl.define do
name
project
- after(:create) do |protected_branch|
- protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER)
- protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER)
+ after(:build) do |protected_branch|
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MASTER)
end
trait :developers_can_push do
after(:create) do |protected_branch|
- protected_branch.push_access_level.update!(access_level: Gitlab::Access::DEVELOPER)
+ protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
end
end
trait :developers_can_merge do
after(:create) do |protected_branch|
- protected_branch.merge_access_level.update!(access_level: Gitlab::Access::DEVELOPER)
+ protected_branch.merge_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
end
end
trait :no_one_can_push do
after(:create) do |protected_branch|
- protected_branch.push_access_level.update!(access_level: Gitlab::Access::NO_ACCESS)
+ protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
end
end
end
diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb
new file mode 100644
index 00000000000..9763cc0cf15
--- /dev/null
+++ b/spec/factories/user_agent_details.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :user_agent_detail do
+ ip_address '127.0.0.1'
+ user_agent 'AppleWebKit/537.36'
+ association :subject, factory: :issue
+ end
+end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
new file mode 100644
index 00000000000..af86d3c338a
--- /dev/null
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+feature 'test coverage badge' do
+ given!(:user) { create(:user) }
+ given!(:project) { create(:project, :private) }
+
+ given!(:pipeline) do
+ create(:ci_pipeline, project: project,
+ ref: 'master',
+ sha: project.commit.id)
+ end
+
+ context 'when user has access to view badge' do
+ background do
+ project.team << [user, :developer]
+ login_as(user)
+ end
+
+ scenario 'user requests coverage badge image for pipeline' do
+ create_job(coverage: 100, name: 'test:1')
+ create_job(coverage: 90, name: 'test:2')
+
+ show_test_coverage_badge
+
+ expect_coverage_badge('95%')
+ end
+
+ scenario 'user requests coverage badge for specific job' do
+ create_job(coverage: 50, name: 'test:1')
+ create_job(coverage: 50, name: 'test:2')
+ create_job(coverage: 85, name: 'coverage')
+
+ show_test_coverage_badge(job: 'coverage')
+
+ expect_coverage_badge('85%')
+ end
+
+ scenario 'user requests coverage badge for pipeline without coverage' do
+ create_job(coverage: nil, name: 'test')
+
+ show_test_coverage_badge
+
+ expect_coverage_badge('unknown')
+ end
+ end
+
+ context 'when user does not have access to view badge' do
+ background { login_as(user) }
+
+ scenario 'user requests test coverage badge image' do
+ show_test_coverage_badge
+
+ expect(page).to have_http_status(404)
+ end
+ end
+
+ def create_job(coverage:, name:)
+ create(:ci_build, name: name,
+ coverage: coverage,
+ pipeline: pipeline)
+ end
+
+ def show_test_coverage_badge(job: nil)
+ visit coverage_namespace_project_badges_path(
+ project.namespace, project, ref: :master, job: job, format: :svg)
+ end
+
+ def expect_coverage_badge(coverage)
+ svg = Nokogiri::XML.parse(page.body)
+ expect(page.response_headers['Content-Type']).to include('image/svg+xml')
+ expect(svg.at(%Q{text:contains("#{coverage}")})).to be_truthy
+ end
+end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 75166bca119..67a4a5d1ab1 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -9,25 +9,43 @@ feature 'list of badges' do
visit namespace_project_pipelines_settings_path(project.namespace, project)
end
- scenario 'user displays list of badges' do
- expect(page).to have_content 'build status'
- expect(page).to have_content 'Markdown'
- expect(page).to have_content 'HTML'
- expect(page).to have_css('.highlight', count: 2)
- expect(page).to have_xpath("//img[@alt='build status']")
-
- page.within('.highlight', match: :first) do
- expect(page).to have_content 'badges/master/build.svg'
+ scenario 'user wants to see build status badge' do
+ page.within('.build-status') do
+ expect(page).to have_content 'build status'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='build status']")
+
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/build.svg'
+ end
end
end
- scenario 'user changes current ref on badges list page', js: true do
- first('.js-project-refs-dropdown').click
+ scenario 'user wants to see coverage report badge' do
+ page.within('.coverage-report') do
+ expect(page).to have_content 'coverage report'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='coverage report']")
- page.within '.project-refs-form' do
- click_link 'improve/awesome'
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/coverage.svg'
+ end
end
+ end
+
+ scenario 'user changes current ref of build status badge', js: true do
+ page.within('.build-status') do
+ first('.js-project-refs-dropdown').click
- expect(page).to have_content 'badges/improve/awesome/build.svg'
+ page.within '.project-refs-form' do
+ click_link 'improve/awesome'
+ end
+
+ expect(page).to have_content 'badges/improve/awesome/build.svg'
+ end
end
end
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
new file mode 100644
index 00000000000..fe047e00409
--- /dev/null
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'User wants to edit a file', feature: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:commit_params) do
+ {
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ commit_message: "Committing First Update",
+ file_path: ".gitignore",
+ file_content: "First Update",
+ last_commit_sha: Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch,
+ ".gitignore").sha
+ }
+ end
+
+ background do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_edit_blob_path(project.namespace, project,
+ File.join(project.default_branch, '.gitignore'))
+ end
+
+ scenario 'file has been updated since the user opened the edit page' do
+ Files::UpdateService.new(project, user, commit_params).execute
+
+ click_button 'Commit Changes'
+
+ expect(page).to have_content 'Someone edited the file the same time you did.'
+ end
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index dbd07464444..a521ce50f35 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -23,7 +23,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
@@ -47,7 +47,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 45bf0c0d038..4453b6d485f 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -23,7 +23,7 @@ feature 'project owner sees a link to create a license file in empty project', f
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 7835e1678ad..f707ccf4e93 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
feature 'project import', feature: true, js: true do
include Select2Helper
- let(:user) { create(:admin) }
- let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+ let(:admin) { create(:admin) }
+ let(:normal_user) { create(:user) }
+ let!(:namespace) { create(:namespace, name: "asd", owner: admin) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
let(:project) { Project.last }
@@ -12,66 +13,87 @@ feature 'project import', feature: true, js: true do
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
- login_as(user)
end
after(:each) do
FileUtils.rm_rf(export_path, secure: true)
end
- scenario 'user imports an exported project successfully' do
- expect(Project.all.count).to be_zero
+ context 'admin user' do
+ before do
+ login_as(admin)
+ end
- visit new_project_path
+ scenario 'user imports an exported project successfully' do
+ expect(Project.all.count).to be_zero
- select2('2', from: '#project_namespace_id')
- fill_in :project_path, with: 'test-project-path', visible: true
- click_link 'GitLab export'
+ visit new_project_path
- expect(page).to have_content('GitLab project export')
- expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: 'test-project-path', visible: true
+ click_link 'GitLab export'
- attach_file('file', file)
+ expect(page).to have_content('GitLab project export')
+ expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
- click_on 'Import project' # import starts
+ attach_file('file', file)
- expect(project).not_to be_nil
- expect(project.issues).not_to be_empty
- expect(project.merge_requests).not_to be_empty
- expect(project_hook).to exist
- expect(wiki_exists?).to be true
- expect(project.import_status).to eq('finished')
- end
+ click_on 'Import project' # import starts
+
+ expect(project).not_to be_nil
+ expect(project.issues).not_to be_empty
+ expect(project.merge_requests).not_to be_empty
+ expect(project_hook).to exist
+ expect(wiki_exists?).to be true
+ expect(project.import_status).to eq('finished')
+ end
- scenario 'invalid project' do
- project = create(:project, namespace_id: 2)
+ scenario 'invalid project' do
+ project = create(:project, namespace_id: 2)
- visit new_project_path
+ visit new_project_path
- select2('2', from: '#project_namespace_id')
- fill_in :project_path, with: project.name, visible: true
- click_link 'GitLab export'
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: project.name, visible: true
+ click_link 'GitLab export'
- attach_file('file', file)
- click_on 'Import project'
+ attach_file('file', file)
+ click_on 'Import project'
- page.within('.flash-container') do
- expect(page).to have_content('Project could not be imported')
+ page.within('.flash-container') do
+ expect(page).to have_content('Project could not be imported')
+ end
+ end
+
+ scenario 'project with no name' do
+ create(:project, namespace_id: 2)
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+
+ # click on disabled element
+ find(:link, 'GitLab export').trigger('click')
+
+ page.within('.flash-container') do
+ expect(page).to have_content('Please enter path and name')
+ end
end
end
- scenario 'project with no name' do
- create(:project, namespace_id: 2)
+ context 'normal user' do
+ before do
+ login_as(normal_user)
+ end
- visit new_project_path
+ scenario 'non-admin user is not allowed to import a project' do
+ expect(Project.all.count).to be_zero
- select2('2', from: '#project_namespace_id')
+ visit new_project_path
- # click on disabled element
- find(:link, 'GitLab export').trigger('click')
+ fill_in :project_path, with: 'test-project-path', visible: true
- page.within('.flash-container') do
- expect(page).to have_content('Please enter path and name')
+ expect(page).not_to have_content('GitLab export')
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
new file mode 100644
index 00000000000..4a83740621a
--- /dev/null
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+feature 'issuable templates', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'user creates an issue using templates' do
+ let(:template_content) { 'this is a test "bug" template' }
+ let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+
+ background do
+ project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ visit edit_namespace_project_issue_path project.namespace, project, issue
+ fill_in :'issue[title]', with: 'test issue title'
+ end
+
+ scenario 'user selects "bug" template' do
+ select_template 'bug'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ context 'user creates a merge request using templates' do
+ let(:template_content) { 'this is a test "feature-proposal" template' }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
+
+ background do
+ project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
+ fill_in :'merge_request[title]', with: 'test merge request title'
+ end
+
+ scenario 'user selects "feature-proposal" template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ context 'user creates a merge request from a forked project using templates' do
+ let(:template_content) { 'this is a test "feature-proposal" template' }
+ let(:fork_user) { create(:user) }
+ let(:fork_project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) }
+
+ background do
+ logout
+ project.team << [fork_user, :developer]
+ fork_project.team << [fork_user, :master]
+ create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
+ login_as fork_user
+ fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request
+ fill_in :'merge_request[title]', with: 'test merge request title'
+ end
+
+ scenario 'user selects "feature-proposal" template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ def preview_template
+ click_link 'Preview'
+ expect(page).to have_content template_content
+ end
+
+ def save_changes
+ click_button "Save changes"
+ expect(page).to have_content template_content
+ end
+
+ def select_template(name)
+ first('.js-issuable-selector').click
+ first('.js-issuable-selector-wrap .dropdown-content a', text: name).click
+ end
+end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 3499460c84d..a0ee6cab7ec 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -71,7 +71,10 @@ feature 'Projected Branches', feature: true, js: true do
project.repository.add_branch(user, 'production-stable', 'master')
project.repository.add_branch(user, 'staging-stable', 'master')
project.repository.add_branch(user, 'development', 'master')
- create(:protected_branch, project: project, name: "*-stable")
+
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
visit namespace_project_protected_branches_path(project.namespace, project)
click_on "2 matching branches"
@@ -90,13 +93,17 @@ feature 'Projected Branches', feature: true, js: true do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
within('.new_protected_branch') do
- find(".js-allowed-to-push").click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ allowed_to_push_button = find(".js-allowed-to-push")
+
+ unless allowed_to_push_button.text == access_type_name
+ allowed_to_push_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
end
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id)
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
end
it "allows updating protected branches so that #{access_type_name} can push to them" do
@@ -112,7 +119,7 @@ feature 'Projected Branches', feature: true, js: true do
end
wait_for_ajax
- expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id)
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
end
end
@@ -121,13 +128,17 @@ feature 'Projected Branches', feature: true, js: true do
visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master')
within('.new_protected_branch') do
- find(".js-allowed-to-merge").click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ allowed_to_merge_button = find(".js-allowed-to-merge")
+
+ unless allowed_to_merge_button.text == access_type_name
+ allowed_to_merge_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
end
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id)
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
end
it "allows updating protected branches so that #{access_type_name} can merge to them" do
@@ -143,7 +154,7 @@ feature 'Projected Branches', feature: true, js: true do
end
wait_for_ajax
- expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id)
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
end
end
end
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 153f1864ceb..9c577501f00 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -38,6 +38,11 @@ describe NotesHelper do
end
describe '#preload_max_access_for_authors' do
+ before do
+ # This method reads cache from RequestStore, so make sure it's clean.
+ RequestStore.clear!
+ end
+
it 'loads multiple users' do
expected_access = {
owner.id => Gitlab::Access::OWNER,
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
deleted file mode 100644
index b08396da4d2..00000000000
--- a/spec/lib/gitlab/akismet_helper_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AkismetHelper, type: :helper do
- let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
-
- before do
- allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true)
- allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345')
- end
-
- describe '#check_for_spam?' do
- it 'returns true for public project' do
- expect(helper.check_for_spam?(project)).to eq(true)
- end
-
- it 'returns false for private project' do
- project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
- expect(helper.check_for_spam?(project)).to eq(false)
- end
- end
-
- describe '#is_spam?' do
- it 'returns true for spam' do
- environment = {
- 'action_dispatch.remote_ip' => '127.0.0.1',
- 'HTTP_USER_AGENT' => 'Test User Agent'
- }
-
- allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true])
- expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true)
- end
- end
-end
diff --git a/spec/lib/gitlab/badge/build/metadata_spec.rb b/spec/lib/gitlab/badge/build/metadata_spec.rb
index ad5388215c2..d678e522721 100644
--- a/spec/lib/gitlab/badge/build/metadata_spec.rb
+++ b/spec/lib/gitlab/badge/build/metadata_spec.rb
@@ -1,37 +1,27 @@
require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
describe Gitlab::Badge::Build::Metadata do
- let(:project) { create(:project) }
- let(:branch) { 'master' }
- let(:badge) { described_class.new(project, branch) }
+ let(:badge) { double(project: create(:project), ref: 'feature') }
+ let(:metadata) { described_class.new(badge) }
- describe '#to_html' do
- let(:html) { Nokogiri::HTML.parse(badge.to_html) }
- let(:a_href) { html.at('a') }
+ it_behaves_like 'badge metadata'
- it 'points to link' do
- expect(a_href[:href]).to eq badge.link_url
- end
-
- it 'contains clickable image' do
- expect(a_href.children.first.name).to eq 'img'
+ describe '#title' do
+ it 'returns build status title' do
+ expect(metadata.title).to eq 'build status'
end
end
- describe '#to_markdown' do
- subject { badge.to_markdown }
-
- it { is_expected.to include badge.image_url }
- it { is_expected.to include badge.link_url }
- end
-
describe '#image_url' do
- subject { badge.image_url }
- it { is_expected.to include "badges/#{branch}/build.svg" }
+ it 'returns valid url' do
+ expect(metadata.image_url).to include 'badges/feature/build.svg'
+ end
end
describe '#link_url' do
- subject { badge.link_url }
- it { is_expected.to include "commits/#{branch}" }
+ it 'returns valid link' do
+ expect(metadata.link_url).to include 'commits/feature'
+ end
end
end
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
index bb8144d5122..38eebb2a176 100644
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -1,11 +1,23 @@
require 'spec_helper'
-describe Gitlab::Badge::Build do
+describe Gitlab::Badge::Build::Status do
let(:project) { create(:project) }
let(:sha) { project.commit.sha }
let(:branch) { 'master' }
let(:badge) { described_class.new(project, branch) }
+ describe '#entity' do
+ it 'always says build' do
+ expect(badge.entity).to eq 'build'
+ end
+ end
+
+ describe '#template' do
+ it 'returns badge template' do
+ expect(badge.template.key_text).to eq 'build'
+ end
+ end
+
describe '#metadata' do
it 'returns badge metadata' do
expect(badge.metadata.image_url)
@@ -13,12 +25,6 @@ describe Gitlab::Badge::Build do
end
end
- describe '#key_text' do
- it 'always says build' do
- expect(badge.key_text).to eq 'build'
- end
- end
-
context 'build exists' do
let!(:build) { create_build(project, sha, branch) }
@@ -30,12 +36,6 @@ describe Gitlab::Badge::Build do
expect(badge.status).to eq 'success'
end
end
-
- describe '#value_text' do
- it 'returns correct value text' do
- expect(badge.value_text).to eq 'success'
- end
- end
end
context 'build failed' do
@@ -46,12 +46,6 @@ describe Gitlab::Badge::Build do
expect(badge.status).to eq 'failed'
end
end
-
- describe '#value_text' do
- it 'has correct value text' do
- expect(badge.value_text).to eq 'failed'
- end
- end
end
context 'when outdated pipeline for given ref exists' do
@@ -87,12 +81,6 @@ describe Gitlab::Badge::Build do
expect(badge.status).to eq 'unknown'
end
end
-
- describe '#value_text' do
- it 'has correct value text' do
- expect(badge.value_text).to eq 'unknown'
- end
- end
end
def create_build(project, sha, branch)
diff --git a/spec/lib/gitlab/badge/build/template_spec.rb b/spec/lib/gitlab/badge/build/template_spec.rb
index 86dead3c54e..a7e21fb8bb1 100644
--- a/spec/lib/gitlab/badge/build/template_spec.rb
+++ b/spec/lib/gitlab/badge/build/template_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe Gitlab::Badge::Build::Template do
- let(:status) { 'success' }
- let(:template) { described_class.new(status) }
+ let(:badge) { double(entity: 'build', status: 'success') }
+ let(:template) { described_class.new(badge) }
describe '#key_text' do
it 'is always says build' do
@@ -34,15 +34,15 @@ describe Gitlab::Badge::Build::Template do
describe '#value_color' do
context 'when status is success' do
- let(:status) { 'success' }
-
it 'has expected color' do
expect(template.value_color).to eq '#4c1'
end
end
context 'when status is failed' do
- let(:status) { 'failed' }
+ before do
+ allow(badge).to receive(:status).and_return('failed')
+ end
it 'has expected color' do
expect(template.value_color).to eq '#e05d44'
@@ -50,7 +50,9 @@ describe Gitlab::Badge::Build::Template do
end
context 'when status is running' do
- let(:status) { 'running' }
+ before do
+ allow(badge).to receive(:status).and_return('running')
+ end
it 'has expected color' do
expect(template.value_color).to eq '#dfb317'
@@ -58,7 +60,9 @@ describe Gitlab::Badge::Build::Template do
end
context 'when status is unknown' do
- let(:status) { 'unknown' }
+ before do
+ allow(badge).to receive(:status).and_return('unknown')
+ end
it 'has expected color' do
expect(template.value_color).to eq '#9f9f9f'
@@ -66,7 +70,9 @@ describe Gitlab::Badge::Build::Template do
end
context 'when status does not match any known statuses' do
- let(:status) { 'invalid status' }
+ before do
+ allow(badge).to receive(:status).and_return('invalid')
+ end
it 'has expected color' do
expect(template.value_color).to eq '#9f9f9f'
diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
new file mode 100644
index 00000000000..74eaf7eaf8b
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
+
+describe Gitlab::Badge::Coverage::Metadata do
+ let(:badge) do
+ double(project: create(:project), ref: 'feature', job: 'test')
+ end
+
+ let(:metadata) { described_class.new(badge) }
+
+ it_behaves_like 'badge metadata'
+
+ describe '#title' do
+ it 'returns coverage report title' do
+ expect(metadata.title).to eq 'coverage report'
+ end
+ end
+
+ describe '#image_url' do
+ it 'returns valid url' do
+ expect(metadata.image_url).to include 'badges/feature/coverage.svg'
+ end
+ end
+
+ describe '#link_url' do
+ it 'returns valid link' do
+ expect(metadata.link_url).to include 'commits/feature'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
new file mode 100644
index 00000000000..1ff49602486
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Report do
+ let(:project) { create(:project) }
+ let(:job_name) { nil }
+
+ let(:badge) do
+ described_class.new(project, 'master', job_name)
+ end
+
+ describe '#entity' do
+ it 'describes a coverage' do
+ expect(badge.entity).to eq 'coverage'
+ end
+ end
+
+ describe '#metadata' do
+ it 'returns correct metadata' do
+ expect(badge.metadata.image_url).to include 'coverage.svg'
+ end
+ end
+
+ describe '#template' do
+ it 'returns correct template' do
+ expect(badge.template.key_text).to eq 'coverage'
+ end
+ end
+
+ shared_examples 'unknown coverage report' do
+ context 'particular job specified' do
+ let(:job_name) { '' }
+
+ it 'returns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+
+ context 'particular job not specified' do
+ let(:job_name) { nil }
+
+ it 'returns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+ end
+
+ context 'pipeline exists' do
+ let!(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: 'master')
+ end
+
+ context 'builds exist' do
+ before do
+ create(:ci_build, name: 'first', pipeline: pipeline, coverage: 40)
+ create(:ci_build, pipeline: pipeline, coverage: 60)
+ end
+
+ context 'particular job specified' do
+ let(:job_name) { 'first' }
+
+ it 'returns coverage for the particular job' do
+ expect(badge.status).to eq 40
+ end
+ end
+
+ context 'particular job not specified' do
+ let(:job_name) { '' }
+
+ it 'returns arithemetic mean for the pipeline' do
+ expect(badge.status).to eq 50
+ end
+ end
+ end
+
+ context 'builds do not exist' do
+ it_behaves_like 'unknown coverage report'
+
+ context 'particular job specified' do
+ let(:job_name) { 'nonexistent' }
+
+ it 'retruns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'pipeline does not exist' do
+ it_behaves_like 'unknown coverage report'
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb
new file mode 100644
index 00000000000..383bae6e087
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/template_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Template do
+ let(:badge) { double(entity: 'coverage', status: 90) }
+ let(:template) { described_class.new(badge) }
+
+ describe '#key_text' do
+ it 'is always says coverage' do
+ expect(template.key_text).to eq 'coverage'
+ end
+ end
+
+ describe '#value_text' do
+ context 'when coverage is known' do
+ it 'returns coverage percentage' do
+ expect(template.value_text).to eq '90%'
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'returns string that says coverage is unknown' do
+ expect(template.value_text).to eq 'unknown'
+ end
+ end
+ end
+
+ describe '#key_width' do
+ it 'has a fixed key width' do
+ expect(template.key_width).to eq 62
+ end
+ end
+
+ describe '#value_width' do
+ context 'when coverage is known' do
+ it 'is narrower when coverage is known' do
+ expect(template.value_width).to eq 36
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'is wider when coverage is unknown to fit text' do
+ expect(template.value_width).to eq 58
+ end
+ end
+ end
+
+ describe '#key_color' do
+ it 'always has the same color' do
+ expect(template.key_color).to eq '#555'
+ end
+ end
+
+ describe '#value_color' do
+ context 'when coverage is good' do
+ before do
+ allow(badge).to receive(:status).and_return(98)
+ end
+
+ it 'is green' do
+ expect(template.value_color).to eq '#4c1'
+ end
+ end
+
+ context 'when coverage is acceptable' do
+ before do
+ allow(badge).to receive(:status).and_return(90)
+ end
+
+ it 'is green-orange' do
+ expect(template.value_color).to eq '#a3c51c'
+ end
+ end
+
+ context 'when coverage is medium' do
+ before do
+ allow(badge).to receive(:status).and_return(75)
+ end
+
+ it 'is orange-yellow' do
+ expect(template.value_color).to eq '#dfb317'
+ end
+ end
+
+ context 'when coverage is low' do
+ before do
+ allow(badge).to receive(:status).and_return(50)
+ end
+
+ it 'is red' do
+ expect(template.value_color).to eq '#e05d44'
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'is grey' do
+ expect(template.value_color).to eq '#9f9f9f'
+ end
+ end
+ end
+
+ describe '#width' do
+ context 'when coverage is known' do
+ it 'returns the key width plus value width' do
+ expect(template.width).to eq 98
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'returns key width plus wider value width' do
+ expect(template.width).to eq 120
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb
new file mode 100644
index 00000000000..0cf18514251
--- /dev/null
+++ b/spec/lib/gitlab/badge/shared/metadata.rb
@@ -0,0 +1,21 @@
+shared_examples 'badge metadata' do
+ describe '#to_html' do
+ let(:html) { Nokogiri::HTML.parse(metadata.to_html) }
+ let(:a_href) { html.at('a') }
+
+ it 'points to link' do
+ expect(a_href[:href]).to eq metadata.link_url
+ end
+
+ it 'contains clickable image' do
+ expect(a_href.children.first.name).to eq 'img'
+ end
+ end
+
+ describe '#to_markdown' do
+ subject { metadata.to_markdown }
+
+ it { is_expected.to include metadata.image_url }
+ it { is_expected.to include metadata.link_url }
+ end
+end
diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 23ae5cfacc4..6c71e98066b 100644
--- a/spec/lib/gitlab/build_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-describe 'Gitlab::BuildDataBuilder' do
+describe Gitlab::DataBuilder::Build do
let(:build) { create(:ci_build) }
describe '.build' do
let(:data) do
- Gitlab::BuildDataBuilder.build(build)
+ described_class.build(build)
end
it { expect(data).to be_a(Hash) }
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 3d6bcdfd873..9a4dec91e56 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe 'Gitlab::NoteDataBuilder', lib: true do
+describe Gitlab::DataBuilder::Note, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
+ let(:data) { described_class.build(note, user) }
let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
before(:each) do
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
new file mode 100644
index 00000000000..a68f5943a6a
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Pipeline do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ status: 'success',
+ sha: project.commit.sha,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ describe '.build' do
+ let(:data) { described_class.build(pipeline) }
+ let(:attributes) { data[:object_attributes] }
+ let(:build_data) { data[:builds].first }
+ let(:project_data) { data[:project] }
+
+ it { expect(attributes).to be_a(Hash) }
+ it { expect(attributes[:ref]).to eq(pipeline.ref) }
+ it { expect(attributes[:sha]).to eq(pipeline.sha) }
+ it { expect(attributes[:tag]).to eq(pipeline.tag) }
+ it { expect(attributes[:id]).to eq(pipeline.id) }
+ it { expect(attributes[:status]).to eq(pipeline.status) }
+
+ it { expect(build_data).to be_a(Hash) }
+ it { expect(build_data[:id]).to eq(build.id) }
+ it { expect(build_data[:status]).to eq(build.status) }
+
+ it { expect(project_data).to eq(project.hook_attrs(backward: false)) }
+ end
+end
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index 6bd7393aaa7..b73434e8dd7 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::PushDataBuilder, lib: true do
+describe Gitlab::DataBuilder::Push, lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index b76e14deca1..b6dec41d218 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::ImportExport::Reader, lib: true do
except: [:iid],
include: [:merge_request_diff, :merge_request_test]
} },
- { commit_statuses: { include: :commit } }]
+ { commit_statuses: { include: :commit } },
+ { project_members: { include: { user: { only: [:email] } } } }]
}
end
diff --git a/spec/lib/gitlab/template/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb
index bc0ec9325cc..9750a012e22 100644
--- a/spec/lib/gitlab/template/gitignore_spec.rb
+++ b/spec/lib/gitlab/template/gitignore_template_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Template::Gitignore do
+describe Gitlab::Template::GitignoreTemplate do
subject { described_class }
describe '.all' do
@@ -24,7 +24,7 @@ describe Gitlab::Template::Gitignore do
it 'returns the Gitignore object of a valid file' do
ruby = subject.find('Ruby')
- expect(ruby).to be_a Gitlab::Template::Gitignore
+ expect(ruby).to be_a Gitlab::Template::GitignoreTemplate
expect(ruby.name).to eq('Ruby')
end
end
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
new file mode 100644
index 00000000000..e3b8321eda3
--- /dev/null
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Template::GitlabCiYmlTemplate do
+ subject { described_class }
+
+ describe '.all' do
+ it 'strips the gitlab-ci suffix' do
+ expect(subject.all.first.name).not_to end_with('.gitlab-ci.yml')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all.map(&:name)
+
+ expect(all).to include('Elixir')
+ expect(all).to include('Docker')
+ expect(all).to include('Ruby')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect(subject.find('mepmep-yadida')).to be nil
+ end
+
+ it 'returns the GitlabCiYml object of a valid file' do
+ ruby = subject.find('Ruby')
+
+ expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate
+ expect(ruby.name).to eq('Ruby')
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ gitignore = subject.new(Rails.root.join('vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml'))
+
+ expect(gitignore.name).to eq 'Ruby'
+ expect(gitignore.content).to start_with('#')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
new file mode 100644
index 00000000000..f770857e958
--- /dev/null
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::IssueTemplate do
+ subject { described_class }
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+ let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
+ let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
+ project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+ end
+
+ describe '.all' do
+ it 'strips the md suffix' do
+ expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all(project).map(&:name)
+
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ it 'returns the issue object of a valid file' do
+ ruby = subject.find('bug', project)
+
+ expect(ruby).to be_a Gitlab::Template::IssueTemplate
+ expect(ruby.name).to eq('bug')
+ end
+ end
+
+ describe '.by_category' do
+ it 'return array of templates' do
+ all = subject.by_category('', project).map(&:name)
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+
+ context 'when repo is bare or empty' do
+ let(:empty_project) { create(:empty_project) }
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "returns empty array" do
+ templates = subject.by_category('', empty_project)
+ expect(templates).to be_empty
+ end
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ issue_template = subject.new('.gitlab/issue_templates/bug.md', project)
+
+ expect(issue_template.name).to eq 'bug'
+ expect(issue_template.content).to eq('something valid')
+ end
+
+ it 'raises error when file is not found' do
+ issue_template = subject.new('.gitlab/issue_templates/bugnot.md', project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ context "when repo is empty" do
+ let(:empty_project) { create(:empty_project) }
+
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "raises file not found" do
+ issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
new file mode 100644
index 00000000000..bb0f68043fa
--- /dev/null
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::MergeRequestTemplate do
+ subject { described_class }
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
+ let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
+ let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
+ project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+ end
+
+ describe '.all' do
+ it 'strips the md suffix' do
+ expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all(project).map(&:name)
+
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ it 'returns the merge request object of a valid file' do
+ ruby = subject.find('bug', project)
+
+ expect(ruby).to be_a Gitlab::Template::MergeRequestTemplate
+ expect(ruby.name).to eq('bug')
+ end
+ end
+
+ describe '.by_category' do
+ it 'return array of templates' do
+ all = subject.by_category('', project).map(&:name)
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+
+ context 'when repo is bare or empty' do
+ let(:empty_project) { create(:empty_project) }
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "returns empty array" do
+ templates = subject.by_category('', empty_project)
+ expect(templates).to be_empty
+ end
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ issue_template = subject.new('.gitlab/merge_request_templates/bug.md', project)
+
+ expect(issue_template.name).to eq 'bug'
+ expect(issue_template.content).to eq('something valid')
+ end
+
+ it 'raises error when file is not found' do
+ issue_template = subject.new('.gitlab/merge_request_templates/bugnot.md', project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ context "when repo is empty" do
+ let(:empty_project) { create(:empty_project) }
+
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "raises file not found" do
+ issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ef55c597642..c45c2635cf4 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -42,7 +42,7 @@ describe Ci::Build, models: true do
describe '#ignored?' do
subject { build.ignored? }
- context 'if build is not allowed to fail' do
+ context 'when build is not allowed to fail' do
before do
build.allow_failure = false
end
@@ -64,7 +64,7 @@ describe Ci::Build, models: true do
end
end
- context 'if build is allowed to fail' do
+ context 'when build is allowed to fail' do
before do
build.allow_failure = true
end
@@ -92,7 +92,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_empty }
- context 'if build.trace contains text' do
+ context 'when build.trace contains text' do
let(:text) { 'example output' }
before do
build.trace = text
@@ -102,7 +102,7 @@ describe Ci::Build, models: true do
it { expect(subject.length).to be >= text.length }
end
- context 'if build.trace hides token' do
+ context 'when build.trace hides token' do
let(:token) { 'my_secret_token' }
before do
@@ -283,13 +283,13 @@ describe Ci::Build, models: true do
stub_ci_pipeline_yaml_file(config)
end
- context 'if config is not found' do
+ context 'when config is not found' do
let(:config) { nil }
it { is_expected.to eq(predefined_variables) }
end
- context 'if config does not have a questioned job' do
+ context 'when config does not have a questioned job' do
let(:config) do
YAML.dump({
test_other: {
@@ -301,7 +301,7 @@ describe Ci::Build, models: true do
it { is_expected.to eq(predefined_variables) }
end
- context 'if config has variables' do
+ context 'when config has variables' do
let(:config) do
YAML.dump({
test: {
@@ -393,7 +393,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_falsey }
end
- context 'if there are runner' do
+ context 'when there are runners' do
let(:runner) { create(:ci_runner) }
before do
@@ -423,29 +423,27 @@ describe Ci::Build, models: true do
describe '#stuck?' do
subject { build.stuck? }
- %w(pending).each do |state|
- context "if commit_status.status is #{state}" do
- before do
- build.status = state
- end
-
- it { is_expected.to be_truthy }
+ context "when commit_status.status is pending" do
+ before do
+ build.status = 'pending'
+ end
- context "and there are specific runner" do
- let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
+ it { is_expected.to be_truthy }
- before do
- build.project.runners << runner
- runner.save
- end
+ context "and there are specific runner" do
+ let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
- it { is_expected.to be_falsey }
+ before do
+ build.project.runners << runner
+ runner.save
end
+
+ it { is_expected.to be_falsey }
end
end
- %w(success failed canceled running).each do |state|
- context "if commit_status.status is #{state}" do
+ %w[success failed canceled running].each do |state|
+ context "when commit_status.status is #{state}" do
before do
build.status = state
end
@@ -767,7 +765,7 @@ describe Ci::Build, models: true do
describe '#when' do
subject { build.when }
- context 'if is undefined' do
+ context 'when `when` is undefined' do
before do
build.when = nil
end
@@ -777,13 +775,13 @@ describe Ci::Build, models: true do
stub_ci_pipeline_yaml_file(config)
end
- context 'if config is not found' do
+ context 'when config is not found' do
let(:config) { nil }
it { is_expected.to eq('on_success') }
end
- context 'if config does not have a questioned job' do
+ context 'when config does not have a questioned job' do
let(:config) do
YAML.dump({
test_other: {
@@ -795,7 +793,7 @@ describe Ci::Build, models: true do
it { is_expected.to eq('on_success') }
end
- context 'if config has when' do
+ context 'when config has `when`' do
let(:config) do
YAML.dump({
test: {
@@ -881,7 +879,7 @@ describe Ci::Build, models: true do
subject { build.play }
- it 'enques a build' do
+ it 'enqueues a build' do
is_expected.to be_pending
is_expected.to eq(build)
end
@@ -901,7 +899,7 @@ describe Ci::Build, models: true do
describe '#when' do
subject { build.when }
- context 'if is undefined' do
+ context 'when `when` is undefined' do
before do
build.when = nil
end
@@ -911,13 +909,13 @@ describe Ci::Build, models: true do
stub_ci_pipeline_yaml_file(config)
end
- context 'if config is not found' do
+ context 'when config is not found' do
let(:config) { nil }
it { is_expected.to eq('on_success') }
end
- context 'if config does not have a questioned job' do
+ context 'when config does not have a questioned job' do
let(:config) do
YAML.dump({
test_other: {
@@ -929,7 +927,7 @@ describe Ci::Build, models: true do
it { is_expected.to eq('on_success') }
end
- context 'if config has when' do
+ context 'when config has when' do
let(:config) do
YAML.dump({
test: {
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 25c3f78c3e1..9c8352a0d7e 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Ci::Pipeline, models: true do
let(:project) { FactoryGirl.create :empty_project }
- let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
+ let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
@@ -18,6 +18,8 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
+ it { is_expected.to delegate_method(:stages).to(:statuses) }
+
describe '#valid_commit_sha' do
context 'commit.sha can not start with 00000000' do
before do
@@ -340,4 +342,87 @@ describe Ci::Pipeline, models: true do
it { is_expected.to eq('running') }
end
end
+
+ describe '#execute_hooks' do
+ let!(:build_a) { create_build('a') }
+ let!(:build_b) { create_build('b') }
+
+ let!(:hook) do
+ create(:project_hook, project: project, pipeline_events: enabled)
+ end
+
+ before do
+ ProjectWebHookWorker.drain
+ end
+
+ context 'with pipeline hooks enabled' do
+ let(:enabled) { true }
+
+ before do
+ WebMock.stub_request(:post, hook.url)
+ end
+
+ context 'with multiple builds' do
+ context 'when build is queued' do
+ before do
+ build_a.enqueue
+ build_b.enqueue
+ end
+
+ it 'receive a pending event once' do
+ expect(WebMock).to have_requested_pipeline_hook('pending').once
+ end
+ end
+
+ context 'when build is run' do
+ before do
+ build_a.enqueue
+ build_a.run
+ build_b.enqueue
+ build_b.run
+ end
+
+ it 'receive a running event once' do
+ expect(WebMock).to have_requested_pipeline_hook('running').once
+ end
+ end
+
+ context 'when all builds succeed' do
+ before do
+ build_a.success
+ build_b.success
+ end
+
+ it 'receive a success event once' do
+ expect(WebMock).to have_requested_pipeline_hook('success').once
+ end
+ end
+
+ def have_requested_pipeline_hook(status)
+ have_requested(:post, hook.url).with do |req|
+ json_body = JSON.parse(req.body)
+ json_body['object_attributes']['status'] == status &&
+ json_body['builds'].length == 2
+ end
+ end
+ end
+ end
+
+ context 'with pipeline hooks disabled' do
+ let(:enabled) { false }
+
+ before do
+ build_a.enqueue
+ build_b.enqueue
+ end
+
+ it 'did not execute pipeline_hook after touched' do
+ expect(WebMock).not_to have_requested(:post, hook.url)
+ end
+ end
+
+ def create_build(name)
+ create(:ci_build, :created, pipeline: pipeline, name: name)
+ end
+ end
end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
new file mode 100644
index 00000000000..32935bc0b09
--- /dev/null
+++ b/spec/models/concerns/spammable_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Issue, 'Spammable' do
+ let(:issue) { create(:issue, description: 'Test Desc.') }
+
+ describe 'Associations' do
+ it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
+ end
+
+ describe 'ClassMethods' do
+ it 'should return correct attr_spammable' do
+ expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}")
+ end
+ end
+
+ describe 'InstanceMethods' do
+ it 'should be invalid if spam' do
+ issue = build(:issue, spam: true)
+ expect(issue.valid?).to be_falsey
+ end
+
+ describe '#check_for_spam?' do
+ it 'returns true for public project' do
+ issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ expect(issue.check_for_spam?).to eq(true)
+ end
+
+ it 'returns false for other visibility levels' do
+ expect(issue.check_for_spam?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 00c4e0fb64c..d672d80156c 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -39,7 +39,7 @@ describe AssemblaService, models: true do
token: 'verySecret',
subdomain: 'project_name'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
WebMock.stub_request(:post, @api_url)
end
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
index ca2cd8aa551..0194f9e2563 100644
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe BuildsEmailService do
- let(:data) { Gitlab::BuildDataBuilder.build(create(:ci_build)) }
+ let(:data) do
+ Gitlab::DataBuilder::Build.build(create(:ci_build))
+ end
describe 'Validations' do
context 'when service is active' do
@@ -39,7 +41,7 @@ describe BuildsEmailService do
describe '#test' do
it 'sends email' do
- data = Gitlab::BuildDataBuilder.build(create(:ci_build))
+ data = Gitlab::DataBuilder::Build.build(create(:ci_build))
subject.recipients = 'test@gitlab.com'
expect(BuildEmailWorker).to receive(:perform_async)
@@ -49,7 +51,7 @@ describe BuildsEmailService do
context 'notify only failed builds is true' do
it 'sends email' do
- data = Gitlab::BuildDataBuilder.build(create(:ci_build))
+ data = Gitlab::DataBuilder::Build.build(create(:ci_build))
data[:build_status] = "success"
subject.recipients = 'test@gitlab.com'
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index 1adf93258f3..c76ae21421b 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -54,7 +54,7 @@ describe CampfireService, models: true do
subdomain: 'project-name',
room: 'test-room'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json'
@headers = { 'Content-Type' => 'application/json; charset=utf-8' }
end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 3a8e67438fc..8ef892259f2 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -84,7 +84,9 @@ describe DroneCiService, models: true do
include_context :drone_ci_service
let(:user) { create(:user, username: 'username') }
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
it do
service_hook = double
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index 6518098ceea..d2557019756 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -52,7 +52,7 @@ describe FlowdockService, models: true do
service_hook: true,
token: 'verySecret'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://api.flowdock.com/v1/messages'
WebMock.stub_request(:post, @api_url)
end
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 2c5583bdaa2..3d0b6c9816b 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -55,7 +55,7 @@ describe GemnasiumService, models: true do
token: 'verySecret',
api_key: 'GemnasiumUserApiKey'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
end
it "calls Gemnasium service" do
expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 1b383219eb9..34eafbe555d 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -48,7 +48,9 @@ describe HipchatService, models: true do
let(:project_name) { project.name_with_namespace.gsub(/\s/, '') }
let(:token) { 'verySecret' }
let(:server_url) { 'https://hipchat.example.com'}
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
before(:each) do
allow(hipchat).to receive_messages(
@@ -108,7 +110,15 @@ describe HipchatService, models: true do
end
context 'tag_push events' do
- let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build(
+ project,
+ user,
+ Gitlab::Git::BLANK_SHA,
+ '1' * 40,
+ 'refs/tags/test',
+ [])
+ end
it "calls Hipchat API for tag push events" do
hipchat.execute(push_sample_data)
@@ -185,7 +195,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -217,7 +227,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -244,7 +254,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
hipchat.execute(data)
message = hipchat.send(:create_message, data)
@@ -270,7 +280,7 @@ describe HipchatService, models: true do
end
it "calls Hipchat API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -293,7 +303,7 @@ describe HipchatService, models: true do
context 'build events' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
context 'for failed' do
before { build.drop }
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index b528baaf15c..ffb17fd3259 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -46,25 +46,28 @@ describe IrkerService, models: true do
let(:irker) { IrkerService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:recipients) { '#commits irc://test.net/#test ftp://bad' }
let(:colorize_messages) { '1' }
before do
+ @irker_server = TCPServer.new 'localhost', 0
+
allow(irker).to receive_messages(
active: true,
project: project,
project_id: project.id,
service_hook: true,
- server_host: 'localhost',
- server_port: 6659,
+ server_host: @irker_server.addr[2],
+ server_port: @irker_server.addr[1],
default_irc_uri: 'irc://chat.freenode.net/',
recipients: recipients,
colorize_messages: colorize_messages)
irker.valid?
- @irker_server = TCPServer.new 'localhost', 6659
end
after do
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 342403f6354..9037ca5cc20 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -66,7 +66,7 @@ describe JiraService, models: true do
password: 'gitlab_jira_password'
)
@jira_service.save # will build API URL, as api_url was not specified above
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
# https://github.com/bblimke/webmock#request-with-basic-authentication
@api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 19c0270a493..5959c81577d 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -48,7 +48,9 @@ describe PushoverService, models: true do
let(:pushover) { PushoverService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:api_key) { 'verySecret' }
let(:user_key) { 'verySecret' }
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 45a5f4ef12a..28af68d13b4 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -45,7 +45,9 @@ describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
@@ -195,7 +197,7 @@ describe SlackService, models: true do
it "uses the right channel" do
slack.update_attributes(note_channel: "random")
- note_data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random").
@@ -235,7 +237,7 @@ describe SlackService, models: true do
end
it "calls Slack API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
@@ -249,7 +251,7 @@ describe SlackService, models: true do
end
it "calls Slack API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
@@ -262,7 +264,7 @@ describe SlackService, models: true do
end
it "calls Slack API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
@@ -276,7 +278,7 @@ describe SlackService, models: true do
end
it "calls Slack API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
slack.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1ce306c4f39..49418475cfb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1103,13 +1103,13 @@ describe Project, models: true do
let(:project) { create(:project) }
it 'returns true when the branch matches a protected branch via direct match' do
- project.protected_branches.create!(name: 'foo')
+ create(:protected_branch, project: project, name: "foo")
expect(project.protected_branch?('foo')).to eq(true)
end
it 'returns true when the branch matches a protected branch via wildcard match' do
- project.protected_branches.create!(name: 'production/*')
+ create(:protected_branch, project: project, name: "production/*")
expect(project.protected_branch?('production/some-branch')).to eq(true)
end
@@ -1119,7 +1119,7 @@ describe Project, models: true do
end
it 'returns false when the branch does not match a protected branch via wildcard match' do
- project.protected_branches.create!(name: 'production/*')
+ create(:protected_branch, project: project, name: "production/*")
expect(project.protected_branch?('staging/some-branch')).to eq(false)
end
diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb
new file mode 100644
index 00000000000..a8c25766e73
--- /dev/null
+++ b/spec/models/user_agent_detail_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe UserAgentDetail, type: :model do
+ describe '.submittable?' do
+ it 'is submittable when not already submitted' do
+ detail = build(:user_agent_detail)
+
+ expect(detail.submittable?).to be_truthy
+ end
+
+ it 'is not submittable when already submitted' do
+ detail = build(:user_agent_detail, submitted: true)
+
+ expect(detail.submittable?).to be_falsey
+ end
+ end
+
+ describe '.valid?' do
+ it 'is valid with a subject' do
+ detail = build(:user_agent_detail)
+
+ expect(detail).to be_valid
+ end
+
+ it 'is invalid without a subject' do
+ detail = build(:user_agent_detail, subject: nil)
+
+ expect(detail).not_to be_valid
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index f67acbbef37..54505f6b822 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -895,7 +895,9 @@ describe User, models: true do
subject { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project1) }
- let!(:push_data) { Gitlab::PushDataBuilder.build_sample(project2, subject) }
+ let!(:push_data) do
+ Gitlab::DataBuilder::Push.build_sample(project2, subject)
+ end
let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
before do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 9444138f93d..3fd989dd7a6 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -243,7 +243,7 @@ describe API::API, api: true do
end
it "removes protected branch" do
- project.protected_branches.create(name: branch_name)
+ create(:protected_branch, project: project, name: branch_name)
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('Protected branch cant be removed')
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 3cd4e981fb2..a40e1a93b71 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -531,8 +531,8 @@ describe API::API, api: true do
describe 'POST /projects/:id/issues with spam filtering' do
before do
- allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
end
let(:params) do
@@ -554,7 +554,6 @@ describe API::API, api: true do
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
- expect(spam_logs[0].project_id).to eq(project.id)
end
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 34fac297923..914e88c9487 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -7,9 +7,9 @@ describe API::API, 'ProjectHooks', api: true do
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:hook) do
create(:project_hook,
- project: project, url: "http://example.com",
- push_events: true, merge_requests_events: true, tag_push_events: true,
- issues_events: true, note_events: true, build_events: true,
+ :all_events_enabled,
+ project: project,
+ url: 'http://example.com',
enable_ssl_verification: true)
end
@@ -33,6 +33,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
expect(json_response.first['build_events']).to eq(true)
+ expect(json_response.first['pipeline_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
@@ -91,6 +92,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
expect(json_response['build_events']).to eq(false)
+ expect(json_response['pipeline_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
end
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 68d0f41b489..5bd5b861792 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,50 +3,53 @@ require 'spec_helper'
describe API::Templates, api: true do
include ApiHelpers
- describe 'the Template Entity' do
- before { get api('/gitignores/Ruby') }
+ context 'global templates' do
+ describe 'the Template Entity' do
+ before { get api('/gitignores/Ruby') }
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
- end
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
- describe 'the TemplateList Entity' do
- before { get api('/gitignores') }
+ describe 'the TemplateList Entity' do
+ before { get api('/gitignores') }
- it { expect(json_response.first['name']).not_to be_nil }
- it { expect(json_response.first['content']).to be_nil }
- end
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
- context 'requesting gitignores' do
- describe 'GET /gitignores' do
- it 'returns a list of available gitignore templates' do
- get api('/gitignores')
+ context 'requesting gitignores' do
+ describe 'GET /gitignores' do
+ it 'returns a list of available gitignore templates' do
+ get api('/gitignores')
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
end
end
- end
- context 'requesting gitlab-ci-ymls' do
- describe 'GET /gitlab_ci_ymls' do
- it 'returns a list of available gitlab_ci_ymls' do
- get api('/gitlab_ci_ymls')
+ context 'requesting gitlab-ci-ymls' do
+ describe 'GET /gitlab_ci_ymls' do
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api('/gitlab_ci_ymls')
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).not_to be_nil
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
+ end
end
end
- end
- describe 'GET /gitlab_ci_ymls/Ruby' do
- it 'adds a disclaimer on the top' do
- get api('/gitlab_ci_ymls/Ruby')
+ describe 'GET /gitlab_ci_ymls/Ruby' do
+ it 'adds a disclaimer on the top' do
+ get api('/gitlab_ci_ymls/Ruby')
- expect(response).to have_http_status(200)
- expect(json_response['content']).to start_with("# This file is a template,")
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).not_to be_nil
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
end
end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
new file mode 100644
index 00000000000..d019e50649f
--- /dev/null
+++ b/spec/services/files/update_service_spec.rb
@@ -0,0 +1,84 @@
+require "spec_helper"
+
+describe Files::UpdateService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:new_contents) { "New Content" }
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: new_contents,
+ file_content_encoding: "text",
+ last_commit_sha: last_commit_sha,
+ source_project: project,
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe "#execute" do
+ context "when the file's last commit sha does not match the supplied last_commit_sha" do
+ let(:last_commit_sha) { "foo" }
+
+ it "returns a hash with the correct error message and a :error status " do
+ expect { subject.execute }.
+ to raise_error(Files::UpdateService::FileChangedError,
+ "You are attempting to update a file that has changed since you started editing it.")
+ end
+ end
+
+ context "when the file's last commit sha does match the supplied last_commit_sha" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha }
+
+ it "returns a hash with the :success status " do
+ results = subject.execute
+
+ expect(results).to match({ status: :success })
+ end
+
+ it "updates the file with the new contents" do
+ subject.execute
+
+ results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(results.data).to eq(new_contents)
+ end
+ end
+
+ context "when the last_commit_sha is not supplied" do
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: new_contents,
+ file_content_encoding: "text",
+ source_project: project,
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ }
+ end
+
+ it "returns a hash with the :success status " do
+ results = subject.execute
+
+ expect(results).to match({ status: :success })
+ end
+
+ it "updates the file with the new contents" do
+ subject.execute
+
+ results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(results.data).to eq(new_contents)
+ end
+ end
+ end
+end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 80f6ebac86c..6ac1fa8f182 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -227,8 +227,8 @@ describe GitPushService, services: true do
expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
- expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER)
- expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::MASTER)
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
it "when pushing a branch for the first time with default branch protection disabled" do
@@ -249,8 +249,8 @@ describe GitPushService, services: true do
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
- expect(project.protected_branches.last.push_access_level.access_level).to eq(Gitlab::Access::DEVELOPER)
- expect(project.protected_branches.last.merge_access_level.access_level).to eq(Gitlab::Access::MASTER)
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
@@ -260,8 +260,8 @@ describe GitPushService, services: true do
expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
- expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER)
- expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::DEVELOPER)
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it "when pushing new commits to existing branch" do
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 3ceec506401..17136dee000 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -7,6 +7,8 @@ project_tree:
- :merge_request_test
- commit_statuses:
- :commit
+ - project_members:
+ - :user
included_attributes:
project:
@@ -14,6 +16,8 @@ included_attributes:
- :path
merge_requests:
- :id
+ user:
+ - :email
excluded_attributes:
merge_requests:
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 98deae0a588..788b92c1b84 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -5,7 +5,7 @@ describe BuildEmailWorker do
let(:build) { create(:ci_build) }
let(:user) { create(:user) }
- let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
subject { BuildEmailWorker.new }
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 796751efe8d..eecc32875a5 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -5,7 +5,7 @@ describe EmailsOnPushWorker do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }