summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml81
-rw-r--r--CHANGELOG75
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock10
-rw-r--r--app/assets/javascripts/LabelManager.js.coffee7
-rw-r--r--app/assets/javascripts/application.js.coffee34
-rw-r--r--app/assets/javascripts/awards_handler.coffee2
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js.coffee61
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js.coffee35
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee5
-rw-r--r--app/assets/javascripts/blob/template_selector.js.coffee56
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee18
-rw-r--r--app/assets/javascripts/due_date_select.js.coffee21
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee24
-rw-r--r--app/assets/javascripts/labels_select.js.coffee4
-rw-r--r--app/assets/javascripts/lib/common_utils.js.coffee50
-rw-r--r--app/assets/javascripts/merge_request.js.coffee2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee2
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee8
-rw-r--r--app/assets/javascripts/network/application.js.coffee20
-rw-r--r--app/assets/javascripts/network/branch-graph.js.coffee (renamed from app/assets/javascripts/branch-graph.js.coffee)0
-rw-r--r--app/assets/javascripts/network/network.js.coffee (renamed from app/assets/javascripts/network.js.coffee)0
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee56
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee4
-rw-r--r--app/assets/javascripts/shortcuts_blob.coffee10
-rw-r--r--app/assets/javascripts/sidebar.js.coffee24
-rw-r--r--app/assets/javascripts/users/calendar.js.coffee17
-rw-r--r--app/assets/javascripts/users_select.js.coffee4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss47
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss14
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss199
-rw-r--r--app/assets/stylesheets/framework/variables.scss12
-rw-r--r--app/assets/stylesheets/mailers/devise.scss4
-rw-r--r--app/assets/stylesheets/pages/commits.scss136
-rw-r--r--app/assets/stylesheets/pages/editor.scss3
-rw-r--r--app/assets/stylesheets/pages/issuable.scss12
-rw-r--r--app/assets/stylesheets/pages/labels.scss14
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss12
-rw-r--r--app/assets/stylesheets/pages/profile.scss19
-rw-r--r--app/assets/stylesheets/pages/projects.scss16
-rw-r--r--app/assets/stylesheets/pages/tree.scss4
-rw-r--r--app/controllers/application_controller.rb23
-rw-r--r--app/controllers/dashboard/todos_controller.rb8
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb48
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb42
-rw-r--r--app/controllers/projects/merge_requests_controller.rb17
-rw-r--r--app/controllers/projects/tags_controller.rb6
-rw-r--r--app/controllers/projects/todos_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb45
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/button_helper.rb20
-rw-r--r--app/helpers/ci_status_helper.rb8
-rw-r--r--app/helpers/commits_helper.rb24
-rw-r--r--app/helpers/diff_helper.rb5
-rw-r--r--app/helpers/members_helper.rb6
-rw-r--r--app/helpers/nav_helper.rb20
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb4
-rw-r--r--app/mailers/emails/projects.rb13
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb42
-rw-r--r--app/models/commit_status.rb3
-rw-r--r--app/models/concerns/importable.rb6
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/jira_issue.rb2
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/merge_request.rb7
-rw-r--r--app/models/merge_request_diff.rb3
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/personal_access_token.rb20
-rw-r--r--app/models/project.rb54
-rw-r--r--app/models/repository.rb21
-rw-r--r--app/models/service.rb2
-rw-r--r--app/models/user.rb23
-rw-r--r--app/services/ci/create_builds_service.rb54
-rw-r--r--app/services/ci/create_pipeline_service.rb24
-rw-r--r--app/services/ci/register_build_service.rb23
-rw-r--r--app/services/create_commit_builds_service.rb55
-rw-r--r--app/services/git_hooks_service.rb2
-rw-r--r--app/services/notification_service.rb10
-rw-r--r--app/services/projects/autocomplete_service.rb4
-rw-r--r--app/services/projects/create_service.rb10
-rw-r--r--app/services/projects/import_export/export_service.rb57
-rw-r--r--app/services/projects/import_service.rb25
-rw-r--r--app/services/todo_service.rb19
-rw-r--r--app/views/admin/background_jobs/_head.html.haml14
-rw-r--r--app/views/admin/background_jobs/show.html.haml82
-rw-r--r--app/views/admin/builds/index.html.haml103
-rw-r--r--app/views/admin/dashboard/_head.html.haml22
-rw-r--r--app/views/admin/dashboard/index.html.haml300
-rw-r--r--app/views/admin/groups/index.html.haml72
-rw-r--r--app/views/admin/health_check/show.html.haml93
-rw-r--r--app/views/admin/logs/show.html.haml52
-rw-r--r--app/views/admin/projects/index.html.haml173
-rw-r--r--app/views/admin/users/index.html.haml199
-rw-r--r--app/views/devise/mailer/password_change.html.haml10
-rw-r--r--app/views/devise/mailer/password_change.text.erb7
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.erb8
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.haml12
-rw-r--r--app/views/devise/mailer/reset_password_instructions.text.erb10
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.haml19
-rw-r--r--app/views/devise/mailer/unlock_instructions.text.erb7
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml25
-rw-r--r--app/views/layouts/_collapse_button.html.haml4
-rw-r--r--app/views/layouts/_page.html.haml9
-rw-r--r--app/views/layouts/_search.html.haml25
-rw-r--r--app/views/layouts/admin.html.haml2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/_admin.html.haml63
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml5
-rw-r--r--app/views/notify/project_was_exported_email.html.haml8
-rw-r--r--app/views/notify/project_was_exported_email.text.erb6
-rw-r--r--app/views/notify/project_was_not_exported_email.html.haml9
-rw-r--r--app/views/notify/project_was_not_exported_email.text.erb6
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml105
-rw-r--r--app/views/projects/_last_push.html.haml22
-rw-r--r--app/views/projects/blob/_editor.html.haml10
-rw-r--r--app/views/projects/builds/show.html.haml10
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml30
-rw-r--r--app/views/projects/commits/_commits.html.haml17
-rw-r--r--app/views/projects/commits/show.html.haml11
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/edit.html.haml36
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml8
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml6
-rw-r--r--app/views/projects/milestones/_form.html.haml8
-rw-r--r--app/views/projects/network/show.html.haml12
-rw-r--r--app/views/projects/new.html.haml58
-rw-r--r--app/views/projects/project_members/update.js.haml2
-rw-r--r--app/views/projects/show.html.haml4
-rw-r--r--app/views/projects/tags/index.html.haml15
-rw-r--r--app/views/projects/tree/_blob_item.html.haml5
-rw-r--r--app/views/projects/tree/_tree_item.html.haml6
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml27
-rw-r--r--app/views/shared/members/_member.html.haml5
-rw-r--r--app/workers/gitlab_remove_project_export_worker.rb9
-rw-r--r--app/workers/project_export_worker.rb12
-rwxr-xr-xbin/spring2
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/default_url_options.rb1
-rw-r--r--config/routes.rb15
-rw-r--r--db/migrate/20160415062917_create_personal_access_tokens.rb13
-rw-r--r--db/migrate/20160616084004_change_project_of_environment.rb21
-rw-r--r--db/schema.rb16
-rw-r--r--doc/api/README.md61
-rw-r--r--doc/api/sidekiq_metrics.md152
-rw-r--r--doc/development/instrumentation.md19
-rw-r--r--doc/development/migration_style_guide.md16
-rw-r--r--features/admin/active_tab.feature22
-rw-r--r--features/dashboard/new_project.feature2
-rw-r--r--features/steps/admin/active_tab.rb36
-rw-r--r--features/steps/dashboard/new_project.rb8
-rw-r--r--features/steps/project/source/browse_files.rb6
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/api/sidekiq_metrics.rb90
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb51
-rw-r--r--lib/banzai/filter/external_link_filter.rb13
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb31
-rw-r--r--lib/banzai/filter/upload_link_filter.rb11
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb15
-rw-r--r--lib/container_registry/client.rb11
-rw-r--r--lib/gitlab/backend/grack_auth.rb7
-rw-r--r--lib/gitlab/backend/shell_env.rb28
-rw-r--r--lib/gitlab/ci/config/node/validation_helpers.rb17
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb119
-rw-r--r--lib/gitlab/gitlab_import/project_creator.rb4
-rw-r--r--lib/gitlab/gl_id.rb11
-rw-r--r--lib/gitlab/import_export.rb39
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb47
-rw-r--r--lib/gitlab/import_export/command_line_util.rb40
-rw-r--r--lib/gitlab/import_export/error.rb5
-rw-r--r--lib/gitlab/import_export/file_importer.rb30
-rw-r--r--lib/gitlab/import_export/import_export.yml54
-rw-r--r--lib/gitlab/import_export/importer.rb64
-rw-r--r--lib/gitlab/import_export/members_mapper.rb68
-rw-r--r--lib/gitlab/import_export/project_creator.rb24
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb105
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb29
-rw-r--r--lib/gitlab/import_export/reader.rb117
-rw-r--r--lib/gitlab/import_export/relation_factory.rb128
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb39
-rw-r--r--lib/gitlab/import_export/repo_saver.rb35
-rw-r--r--lib/gitlab/import_export/saver.rb42
-rw-r--r--lib/gitlab/import_export/shared.rb30
-rw-r--r--lib/gitlab/import_export/uploads_restorer.rb14
-rw-r--r--lib/gitlab/import_export/uploads_saver.rb36
-rw-r--r--lib/gitlab/import_export/version_checker.rb36
-rw-r--r--lib/gitlab/import_export/version_saver.rb25
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb33
-rw-r--r--lib/gitlab/import_sources.rb3
-rw-r--r--lib/gitlab/metrics/instrumentation.rb19
-rw-r--r--lib/gitlab/metrics/method_call.rb52
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb6
-rw-r--r--lib/gitlab/metrics/transaction.rb35
-rw-r--r--lib/gitlab/workhorse.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb71
-rw-r--r--spec/factories/personal_access_tokens.rb9
-rw-r--r--spec/features/admin/admin_hooks_spec.rb4
-rw-r--r--spec/features/issues_spec.rb4
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb94
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb14
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb12
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb49
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin0 -> 345686 bytes
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb1
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb17
-rw-r--r--spec/features/search_spec.rb79
-rw-r--r--spec/finders/notes_finder_spec.rb7
-rw-r--r--spec/helpers/members_helper_spec.rb16
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb6
-rw-r--r--spec/javascripts/fixtures/search_autocomplete.html.haml10
-rw-r--r--spec/javascripts/merge_request_spec.js.coffee2
-rw-r--r--spec/javascripts/notes_spec.js.coffee2
-rw-r--r--spec/javascripts/project_title_spec.js.coffee2
-rw-r--r--spec/javascripts/search_autocomplete_spec.js.coffee149
-rw-r--r--spec/lib/banzai/filter/abstract_link_filter_spec.rb52
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb34
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb9
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb20
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb115
-rw-r--r--spec/lib/container_registry/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/project.json5341
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb23
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb149
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb87
-rw-r--r--spec/lib/gitlab/import_export/repo_bundler_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb28
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb10
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb91
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb16
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb16
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb3
-rw-r--r--spec/mailers/previews/devise_mailer_preview.rb23
-rw-r--r--spec/models/ci/pipeline_spec.rb13
-rw-r--r--spec/models/group_spec.rb12
-rw-r--r--spec/models/jira_issue_spec.rb30
-rw-r--r--spec/models/personal_access_token_spec.rb15
-rw-r--r--spec/models/project_services/jira_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb18
-rw-r--r--spec/models/repository_spec.rb41
-rw-r--r--spec/requests/api/api_helpers_spec.rb76
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb40
-rw-r--r--spec/services/ci/create_builds_service_spec.rb6
-rw-r--r--spec/services/ci/register_build_service_spec.rb62
-rw-r--r--spec/services/create_commit_builds_service_spec.rb23
-rw-r--r--spec/services/git_push_service_spec.rb3
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/todo_service_spec.rb106
-rw-r--r--spec/support/import_export/import_export.yml20
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb12
-rw-r--r--vendor/assets/javascripts/raphael.js8239
273 files changed, 19415 insertions, 1706 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 83a906932d0..219077d79b8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -129,56 +129,51 @@ spinach 7 10: *spinach-knapsack
spinach 8 10: *spinach-knapsack
spinach 9 10: *spinach-knapsack
-# Execute all testing suites against Ruby 2.2
-
-.ruby-22: &ruby-22
- image: "ruby:2.2"
+# Execute all testing suites against Ruby 2.3
+.ruby-23: &ruby-23
+ image: "ruby:2.3"
only:
- master
- cache:
- key: "ruby22"
- paths:
- - vendor
-.rspec-knapsack-ruby22: &rspec-knapsack-ruby22
+.rspec-knapsack-ruby23: &rspec-knapsack-ruby23
<<: *rspec-knapsack
- <<: *ruby-22
+ <<: *ruby-23
-.spinach-knapsack-ruby22: &spinach-knapsack-ruby22
+.spinach-knapsack-ruby23: &spinach-knapsack-ruby23
<<: *spinach-knapsack
- <<: *ruby-22
+ <<: *ruby-23
-rspec 0 20 ruby22: *rspec-knapsack-ruby22
-rspec 1 20 ruby22: *rspec-knapsack-ruby22
-rspec 2 20 ruby22: *rspec-knapsack-ruby22
-rspec 3 20 ruby22: *rspec-knapsack-ruby22
-rspec 4 20 ruby22: *rspec-knapsack-ruby22
-rspec 5 20 ruby22: *rspec-knapsack-ruby22
-rspec 6 20 ruby22: *rspec-knapsack-ruby22
-rspec 7 20 ruby22: *rspec-knapsack-ruby22
-rspec 8 20 ruby22: *rspec-knapsack-ruby22
-rspec 9 20 ruby22: *rspec-knapsack-ruby22
-rspec 10 20 ruby22: *rspec-knapsack-ruby22
-rspec 11 20 ruby22: *rspec-knapsack-ruby22
-rspec 12 20 ruby22: *rspec-knapsack-ruby22
-rspec 13 20 ruby22: *rspec-knapsack-ruby22
-rspec 14 20 ruby22: *rspec-knapsack-ruby22
-rspec 15 20 ruby22: *rspec-knapsack-ruby22
-rspec 16 20 ruby22: *rspec-knapsack-ruby22
-rspec 17 20 ruby22: *rspec-knapsack-ruby22
-rspec 18 20 ruby22: *rspec-knapsack-ruby22
-rspec 19 20 ruby22: *rspec-knapsack-ruby22
-
-spinach 0 10 ruby22: *spinach-knapsack-ruby22
-spinach 1 10 ruby22: *spinach-knapsack-ruby22
-spinach 2 10 ruby22: *spinach-knapsack-ruby22
-spinach 3 10 ruby22: *spinach-knapsack-ruby22
-spinach 4 10 ruby22: *spinach-knapsack-ruby22
-spinach 5 10 ruby22: *spinach-knapsack-ruby22
-spinach 6 10 ruby22: *spinach-knapsack-ruby22
-spinach 7 10 ruby22: *spinach-knapsack-ruby22
-spinach 8 10 ruby22: *spinach-knapsack-ruby22
-spinach 9 10 ruby22: *spinach-knapsack-ruby22
+rspec 0 20 ruby23: *rspec-knapsack-ruby23
+rspec 1 20 ruby23: *rspec-knapsack-ruby23
+rspec 2 20 ruby23: *rspec-knapsack-ruby23
+rspec 3 20 ruby23: *rspec-knapsack-ruby23
+rspec 4 20 ruby23: *rspec-knapsack-ruby23
+rspec 5 20 ruby23: *rspec-knapsack-ruby23
+rspec 6 20 ruby23: *rspec-knapsack-ruby23
+rspec 7 20 ruby23: *rspec-knapsack-ruby23
+rspec 8 20 ruby23: *rspec-knapsack-ruby23
+rspec 9 20 ruby23: *rspec-knapsack-ruby23
+rspec 10 20 ruby23: *rspec-knapsack-ruby23
+rspec 11 20 ruby23: *rspec-knapsack-ruby23
+rspec 12 20 ruby23: *rspec-knapsack-ruby23
+rspec 13 20 ruby23: *rspec-knapsack-ruby23
+rspec 14 20 ruby23: *rspec-knapsack-ruby23
+rspec 15 20 ruby23: *rspec-knapsack-ruby23
+rspec 16 20 ruby23: *rspec-knapsack-ruby23
+rspec 17 20 ruby23: *rspec-knapsack-ruby23
+rspec 18 20 ruby23: *rspec-knapsack-ruby23
+rspec 19 20 ruby23: *rspec-knapsack-ruby23
+
+spinach 0 10 ruby23: *spinach-knapsack-ruby23
+spinach 1 10 ruby23: *spinach-knapsack-ruby23
+spinach 2 10 ruby23: *spinach-knapsack-ruby23
+spinach 3 10 ruby23: *spinach-knapsack-ruby23
+spinach 4 10 ruby23: *spinach-knapsack-ruby23
+spinach 5 10 ruby23: *spinach-knapsack-ruby23
+spinach 6 10 ruby23: *spinach-knapsack-ruby23
+spinach 7 10 ruby23: *spinach-knapsack-ruby23
+spinach 8 10 ruby23: *spinach-knapsack-ruby23
+spinach 9 10 ruby23: *spinach-knapsack-ruby23
# Other generic tests
diff --git a/CHANGELOG b/CHANGELOG
index bb5bde9b08b..e16f5c331e5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,8 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
+ - Fix error when CI job variables key specified but not defined
+ - Fix pipeline status when there are no builds in pipeline
- Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues.
@@ -12,6 +14,8 @@ v 8.9.0 (unreleased)
- Fix an issue where note polling stopped working if a window was in the
background during a refresh.
- Make EmailsOnPushWorker use Sidekiq mailers queue
+ - Redesign all Devise emails. !4297
+ - Don't show 'Leave Project' to group members
- Fix wiki page events' webhook to point to the wiki repository
- Don't show tags for revert and cherry-pick operations
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
@@ -22,12 +26,17 @@ v 8.9.0 (unreleased)
- Added descriptions to notification settings dropdown
- Improve note validation to prevent errors when creating invalid note via API
- Reduce number of fog gem dependencies
+ - Implement a fair usage of shared runners
- Remove project notification settings associated with deleted projects
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects
- Add a metric for the number of new Redis connections created by a transaction
- Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
- Redesign navigation for project pages
+ - Fix images in sign-up confirmation email
+ - Added shortcut 'y' for copying a files content hash URL #14470
- Fix groups API to list only user's accessible projects
+ - Fix horizontal scrollbar for long commit message.
+ - GitLab Performance Monitoring now tracks the total method execution time and call count per method
- Add Environments and Deployments
- Redesign account and email confirmation emails
- Don't fail builds for projects that are deleted
@@ -35,29 +44,39 @@ v 8.9.0 (unreleased)
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0
+ - Fixed alignment of download dropdown in merge requests
- Upgrade to jQuery 2
+ - Adds selected branch name to the dropdown toggle
+ - Add API endpoint for Sidekiq Metrics !4653
- Use Knapsack to evenly distribute tests across multiple nodes
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load
- Add DB index on users.state
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
+ - Fix race condition on merge when build succeeds
- Links from a wiki page to other wiki pages should be rewritten as expected
- Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
+ - Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393
- Fix issues filter when ordering by milestone
- Added artifacts:when to .gitlab-ci.yml - this requires GitLab Runner 1.3
- Bamboo Service: Fix missing credentials & URL handling when base URL contains a path (Benjamin Schmid)
- TeamCity Service: Fix URL handling when base URL contains a path
- Todos will display target state if issuable target is 'Closed' or 'Merged'
+ - Validate only and except regexp
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature
+ - Toggle whitespace button now available for compare branches diffs #17881
- Pipelines can be canceled only when there are running builds
+ - Allow authentication using personal access tokens
- Use downcased path to container repository as this is expected path by Docker
- Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails
+ - Added Gfm autocomplete for labels
- Make Omniauth providers specs to not modify global configuration
+ - Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11
- Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav
@@ -74,8 +93,10 @@ v 8.9.0 (unreleased)
- Replace Colorize with Rainbow for coloring console output in Rake tasks.
- Add workhorse controller and API helpers
- An indicator is now displayed at the top of the comment field for confidential issues.
+ - Show categorised search queries in the search autocomplete
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Improve issuables APIs performance when accessing notes !4471
+ - Add sorting dropdown to tags page !4423
- External links now open in a new tab
- Prevent default actions of disabled buttons and links
- Markdown editor now correctly resets the input value on edit cancellation !4175
@@ -83,6 +104,7 @@ v 8.9.0 (unreleased)
- Improved UX of date pickers on issue & milestone forms
- Cache on the database if a project has an active external issue tracker.
- Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav
+ - GitLab project import and export functionality
- All classes in the Banzai::ReferenceParser namespace are now instrumented
- Remove deprecated issues_tracker and issues_tracker_id from project model
- Allow users to create confidential issues in private projects
@@ -99,18 +121,29 @@ v 8.9.0 (unreleased)
- Remove tanuki logo from side navigation; center on top nav
- Include user relationships when retrieving award_emoji
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
-
-v 8.8.5 (unreleased)
- - Ensure branch cleanup regardless of whether the GitHub import process succeeds
- - Fix todos page throwing errors when you have a project pending deletion
- - Reduce number of SQL queries when rendering user references
- - Import GitHub repositories respecting the API rate limit
- - Fix importer for GitHub comments on diff
- - Disable Webhooks before proceeding with the GitHub import
- - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace
+ - Set inverse_of for Project/Service association to reduce the number of queries
+ - Update tanuki logo highlight/loading colors
+ - Use Git cached counters for branches and tags on project page
+ - Filter parameters for request_uri value on instrumented transactions.
+ - Cache user todo counts from TodoService
+
+v 8.8.5
+ - Import GitHub repositories respecting the API rate limit !4166
+ - Fix todos page throwing errors when you have a project pending deletion !4300
+ - Disable Webhooks before proceeding with the GitHub import !4470
+ - Fix importer for GitHub comments on diff !4488
+ - Adjust the SAML control flow to allow LDAP identities to be added to an existing SAML user !4498
+ - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace !4541
+ - Prevent unauthorized access for projects build traces
+ - Forbid scripting for wiki files
+ - Only show notes through JSON on confidential issues that the user has access to
+ - Banzai::Filter::UploadLinkFilter use XPath instead CSS expressions
+ - Banzai::Filter::ExternalLinkFilter use XPath instead CSS expressions
v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493
+ - Added descriptions to notification settings dropdown
+ - Due date can be removed from milestones
v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
@@ -226,6 +259,9 @@ v 8.8.0
v 8.7.7
- Fix import by `Any Git URL` broken if the URL contains a space
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+ - Only show notes through JSON on confidential issues that the user has access to
v 8.7.6
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
@@ -388,6 +424,11 @@ v 8.7.0
- Add RAW build trace output and button on build page
- Add incremental build trace update into CI API
+v 8.6.9
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+ - Only show notes through JSON on confidential issues that the user has access to
+
v 8.6.8
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -542,6 +583,10 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
+v 8.5.13
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.5.12
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -703,6 +748,10 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
+v 8.4.11
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.4.10
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -839,6 +888,10 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
+v 8.3.10
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.3.9
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
@@ -957,6 +1010,10 @@ v 8.3.0
- Expose Git's version in the admin area
- Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
+v 8.2.6
+ - Prevent unauthorized access to other projects build traces
+ - Forbid scripting for wiki files
+
v 8.2.5
- Prevent privilege escalation via "impersonate" feature
- Prevent privilege escalation via notes API
diff --git a/Gemfile b/Gemfile
index 3b287893002..bc1223e1bbc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -52,7 +52,7 @@ gem "browser", '~> 2.0.3'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 10.0'
+gem "gitlab_git", '~> 10.2'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -221,13 +221,12 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
-gem 'font-awesome-rails', '~> 4.2'
+gem 'font-awesome-rails', '~> 4.6.1'
gem 'gitlab_emoji', '~> 0.3.0'
gem 'gon', '~> 6.0.1'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0'
-gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index c1c8c175b1d..49e548fb94f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -246,7 +246,7 @@ GEM
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
- font-awesome-rails (4.5.0.1)
+ font-awesome-rails (4.6.1.0)
railties (>= 3.2, < 5.1)
foreman (0.78.0)
thor (~> 0.19.1)
@@ -277,7 +277,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (10.1.3)
+ gitlab_git (10.2.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -556,7 +556,6 @@ GEM
rainbow (2.1.0)
raindrops (0.15.0)
rake (10.5.0)
- raphael-rails (2.1.2)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
@@ -867,7 +866,7 @@ DEPENDENCIES
fog-google (~> 0.3)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
- font-awesome-rails (~> 4.2)
+ font-awesome-rails (~> 4.6.1)
foreman
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
@@ -875,7 +874,7 @@ DEPENDENCIES
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0)
- gitlab_git (~> 10.0)
+ gitlab_git (~> 10.2)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
@@ -938,7 +937,6 @@ DEPENDENCIES
rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
- raphael-rails (~> 2.1.2)
rblineprof
rdoc (~> 3.6)
recaptcha (~> 3.0)
diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee
index 365a062bb81..b06bcf0fcbf 100644
--- a/app/assets/javascripts/LabelManager.js.coffee
+++ b/app/assets/javascripts/LabelManager.js.coffee
@@ -42,10 +42,10 @@ class @LabelManager
$from = @prioritizedLabels
if $from.find('li').length is 1
- $from.find('.empty-message').show()
+ $from.find('.empty-message').removeClass('hidden')
if not $target.find('li').length
- $target.find('.empty-message').hide()
+ $target.find('.empty-message').addClass('hidden')
$label.detach().appendTo($target)
@@ -54,6 +54,9 @@ class @LabelManager
if action is 'remove'
xhr = $.ajax url: url, type: 'DELETE'
+
+ # Restore empty message
+ $from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
else
xhr = @savePrioritySort($label, action)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 6c16f89cef6..2f9f6c3ef5b 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -32,10 +32,6 @@
#= require bootstrap/tooltip
#= require bootstrap/popover
#= require select2
-#= require raphael
-#= require g.raphael
-#= require g.bar
-#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
#= require underscore
@@ -128,7 +124,7 @@ $ ->
gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize()
- $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
+ $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
# Click a .js-select-on-focus field, select the contents
$(".js-select-on-focus").on "focusin", ->
@@ -258,3 +254,31 @@ $ ->
gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize()
new Aside()
+
+ # Sidenav pinning
+ if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
+ $.cookie('pin_nav', 'false')
+ $('.page-with-sidebar')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+ .removeClass('page-sidebar-pinned')
+ $('.navbar-fixed-top').removeClass('header-pinned-nav')
+
+ $(document)
+ .off 'click', '.js-nav-pin'
+ .on 'click', '.js-nav-pin', (e) ->
+ e.preventDefault()
+
+ $(this).toggleClass 'is-active'
+
+ if $.cookie('pin_nav') is 'true'
+ $.cookie 'pin_nav', 'false'
+ $('.page-with-sidebar')
+ .removeClass('page-sidebar-pinned')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+ $('.navbar-fixed-top')
+ .removeClass('header-pinned-nav')
+ .toggleClass('header-collapsed header-expanded')
+ else
+ $.cookie 'pin_nav', 'true'
+ $('.page-with-sidebar').addClass('page-sidebar-pinned')
+ $('.navbar-fixed-top').addClass('header-pinned-nav')
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
index 136db8ee14d..030f1564862 100644
--- a/app/assets/javascripts/awards_handler.coffee
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -40,7 +40,7 @@ class @AwardsHandler
$menu = $ '.emoji-menu'
if $addBtn.hasClass 'js-note-emoji'
- $addBtn.parents('.note').find('.js-awards-block').addClass 'current'
+ $addBtn.closest('.note').find('.js-awards-block').addClass 'current'
else
$addBtn.closest('.js-awards-block').addClass 'current'
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
index cc8a497d081..8d0e3f363d1 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
@@ -1,58 +1,5 @@
-class @BlobGitignoreSelector
- constructor: (opts) ->
- {
- @dropdown
- @editor
- @$wrapper = @dropdown.closest('.gitignore-selector')
- @$filenameInput = $('#file_name')
- @data = @dropdown.data('filenames')
- } = opts
+#= require blob/template_selector
- @dropdown.glDropdown(
- data: @data,
- filterable: true,
- selectable: true,
- search:
- fields: ['name']
- clicked: @onClick
- text: (gitignore) ->
- gitignore.name
- )
-
- @toggleGitignoreSelector()
- @bindEvents()
-
- bindEvents: ->
- @$filenameInput
- .on 'keyup blur', (e) =>
- @toggleGitignoreSelector()
-
- toggleGitignoreSelector: ->
- filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
- @$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
-
- onClick: (item, el, e) =>
- e.preventDefault()
- @requestIgnoreFile(item.name)
-
- requestIgnoreFile: (name) ->
- Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
-
- requestIgnoreFileSuccess: (gitignore) ->
- @editor.setValue(gitignore.content, 1)
- @editor.focus()
-
-class @BlobGitignoreSelectors
- constructor: (opts) ->
- {
- @$dropdowns = $('.js-gitignore-selector')
- @editor
- } = opts
-
- @$dropdowns.each (i, dropdown) =>
- $dropdown = $(dropdown)
-
- new BlobGitignoreSelector(
- dropdown: $dropdown,
- editor: @editor
- )
+class @BlobGitignoreSelector extends TemplateSelector
+ requestFile: (query) ->
+ Api.gitignoreText query.name, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
new file mode 100644
index 00000000000..a719ba25122
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
@@ -0,0 +1,17 @@
+class @BlobGitignoreSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-gitignore-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobGitignoreSelector(
+ pattern: /(.gitignore)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee
index e17eaa75dc1..a3cc8dd844c 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js.coffee
+++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee
@@ -1,30 +1,9 @@
-class @BlobLicenseSelector
- licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
+#= require blob/template_selector
- constructor: (editor) ->
- @$licenseSelector = $('.js-license-selector')
- $fileNameInput = $('#file_name')
+class @BlobLicenseSelector extends TemplateSelector
+ requestFile: (query) ->
+ data =
+ project: @dropdown.data('project')
+ fullname: @dropdown.data('fullname')
- initialFileNameValue = if $fileNameInput.length
- $fileNameInput.val()
- else if $('.editor-file-name').length
- $('.editor-file-name').text().trim()
-
- @toggleLicenseSelector(initialFileNameValue)
-
- if $fileNameInput
- $fileNameInput.on 'keyup blur', (e) =>
- @toggleLicenseSelector($(e.target).val())
-
- $('select.license-select').on 'change', (e) ->
- data =
- project: $(this).data('project')
- fullname: $(this).data('fullname')
- Api.licenseText $(this).val(), data, (license) ->
- editor.setValue(license.content, -1)
-
- toggleLicenseSelector: (fileName) =>
- if @licenseRegex.test(fileName)
- @$licenseSelector.show()
- else
- @$licenseSelector.hide()
+ Api.licenseText query.id, data, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.coffee b/app/assets/javascripts/blob/blob_license_selectors.js.coffee
new file mode 100644
index 00000000000..68438733108
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.coffee
@@ -0,0 +1,17 @@
+class @BlobLicenseSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-license-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobLicenseSelector(
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index 79141e768b8..636f909dbd0 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -12,8 +12,9 @@ class @EditBlob
$("#file-content").val(@editor.getValue())
@initModePanesAndLinks()
- new BlobLicenseSelector(@editor)
- new BlobGitignoreSelectors(editor: @editor)
+
+ new BlobLicenseSelectors { @editor }
+ new BlobGitignoreSelectors { @editor }
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
diff --git a/app/assets/javascripts/blob/template_selector.js.coffee b/app/assets/javascripts/blob/template_selector.js.coffee
new file mode 100644
index 00000000000..e76e303189d
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selector.js.coffee
@@ -0,0 +1,56 @@
+class @TemplateSelector
+ constructor: (opts = {}) ->
+ {
+ @dropdown,
+ @data,
+ @pattern,
+ @wrapper,
+ @editor,
+ @fileEndpoint,
+ @$input = $('#file_name')
+ } = opts
+
+ @buildDropdown()
+ @bindEvents()
+ @onFilenameUpdate()
+
+ buildDropdown: ->
+ @dropdown.glDropdown(
+ data: @data,
+ filterable: true,
+ selectable: true,
+ search:
+ fields: ['name']
+ clicked: @onClick
+ text: (item) ->
+ item.name
+ )
+
+ bindEvents: ->
+ @$input.on('keyup blur', (e) =>
+ @onFilenameUpdate()
+ )
+
+ onFilenameUpdate: ->
+ return unless @$input.length
+
+ filenameMatches = @pattern.test(@$input.val().trim())
+
+ if not filenameMatches
+ @wrapper.addClass('hidden')
+ return
+
+ @wrapper.removeClass('hidden')
+
+ onClick: (item, el, e) =>
+ e.preventDefault()
+ @requestFile(item)
+
+ requestFile: (item) ->
+ # To be implemented on the extending class
+ # e.g.
+ # Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+
+ requestFileSuccess: (file) ->
+ @editor.setValue(file.content, 1)
+ @editor.focus()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 8b39e6b090c..b560500cce6 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -29,6 +29,7 @@ class Dispatcher
new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
+ new DueDateSelect()
new GLForm($('.milestone-form'))
when 'groups:milestones:new'
new ZenMode()
@@ -72,9 +73,7 @@ class Dispatcher
new Diff()
new ZenMode()
shortcut_handler = new ShortcutsNavigation()
- when 'projects:commits:show'
- shortcut_handler = new ShortcutsNavigation()
- when 'projects:activity'
+ when 'projects:commits:show', 'projects:activity'
shortcut_handler = new ShortcutsNavigation()
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
@@ -100,6 +99,7 @@ class Dispatcher
when 'projects:blob:show', 'projects:blame:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
+ new ShortcutsBlob true
when 'projects:labels:new', 'projects:labels:edit'
new Labels()
when 'projects:labels:index'
@@ -133,15 +133,11 @@ class Dispatcher
new Project()
new ProjectAvatar()
switch path[1]
- when 'compare'
- shortcut_handler = new ShortcutsNavigation()
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
- when 'new'
+ when 'new', 'show'
new ProjectNew()
- when 'show'
- new ProjectShow()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
@@ -150,9 +146,9 @@ class Dispatcher
when 'snippets'
shortcut_handler = new ShortcutsNavigation()
new ZenMode() if path[2] == 'show'
- when 'labels', 'graphs'
- shortcut_handler = new ShortcutsNavigation()
- when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
+ when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
+ 'milestones', 'project_members', 'deploy_keys', 'builds', \
+ 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
# If we haven't installed a custom shortcut handler, install the default one
diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee
index 3d009a96d05..d65c018dad5 100644
--- a/app/assets/javascripts/due_date_select.js.coffee
+++ b/app/assets/javascripts/due_date_select.js.coffee
@@ -1,5 +1,21 @@
class @DueDateSelect
constructor: ->
+ # Milestone edit/new form
+ $datePicker = $('.datepicker')
+
+ if $datePicker.length
+ $dueDate = $('#milestone_due_date')
+ $datePicker.datepicker
+ dateFormat: 'yy-mm-dd'
+ onSelect: (dateText, inst) ->
+ $dueDate.val(dateText)
+ .datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()))
+
+ $('.js-clear-due-date').on 'click', (e) ->
+ e.preventDefault()
+ $.datepicker._clearDate($datePicker)
+
+ # Issuable sidebar
$loading = $('.js-issuable-update .due_date')
.find('.block-loading')
.hide()
@@ -32,7 +48,7 @@ class @DueDateSelect
date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date
else
- mediumDate = 'None'
+ mediumDate = 'No due date'
data = {}
data[abilityName] = {}
@@ -50,7 +66,8 @@ class @DueDateSelect
$selectbox.hide()
$value.css('display', '')
- $valueContent.html(mediumDate)
+ cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value'
+ $valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>")
$sidebarValue.html(mediumDate)
if value isnt ''
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 76c3083232b..190bb38504c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -15,6 +15,9 @@ GitLab.GfmAutoComplete =
Members:
template: '<li>${username} <small>${title}</small></li>'
+ Labels:
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+
# Issues and MergeRequests
Issues:
template: '<li><small>${id}</small> ${title}</li>'
@@ -176,6 +179,25 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
+ @input.atwho
+ at: '~'
+ alias: 'labels'
+ searchKey: 'search'
+ displayTpl: @Labels.template
+ insertTpl: '${atwho-at}${title}'
+ callbacks:
+ beforeSave: (merges) ->
+ sanitizeLabelTitle = (title)->
+ if /\w+\s+\w+/g.test(title)
+ "\"#{sanitize(title)}\""
+ else
+ sanitize(title)
+
+ $.map merges, (m) ->
+ title: sanitizeLabelTitle(m.title)
+ color: m.color
+ search: "#{m.title}"
+
destroyAtWho: ->
@input.atwho('destroy')
@@ -195,6 +217,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
@input.atwho 'load', ':', data.emojis
+ # load labels
+ @input.atwho 'load', '~', data.labels
# This trigger at.js again
# otherwise we would be stuck with loading until the user types
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 9ca88f1226e..d350a7c0e7f 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -39,7 +39,7 @@ class @LabelsSelect
</a>
<% }); %>'
)
- labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
+ labelNoneHTMLTemplate = '<span class="no-value">None</span>'
if newLabelField.length
@@ -145,7 +145,7 @@ class @LabelsSelect
template = labelHTMLTemplate(data)
labelCount = data.labels.length
else
- template = labelNoneHTMLTemplate()
+ template = labelNoneHTMLTemplate
$value
.removeAttr('style')
.html(template)
diff --git a/app/assets/javascripts/lib/common_utils.js.coffee b/app/assets/javascripts/lib/common_utils.js.coffee
index 4f1779b8483..e39dcb2daa9 100644
--- a/app/assets/javascripts/lib/common_utils.js.coffee
+++ b/app/assets/javascripts/lib/common_utils.js.coffee
@@ -1,28 +1,27 @@
((w) ->
- window.gl or= {}
- window.gl.utils or= {}
+ w.gl or= {}
+ w.gl.utils or= {}
- jQuery.timefor = (time, suffix, expiredLabel) ->
+ w.gl.utils.isInGroupsPage = ->
- return '' unless time
+ return $('body').data('page').split(':')[0] is 'groups'
- suffix or= 'remaining'
- expiredLabel or= 'Past due'
- jQuery.timeago.settings.allowFuture = yes
+ w.gl.utils.isInProjectPage = ->
- { suffixFromNow } = jQuery.timeago.settings.strings
- jQuery.timeago.settings.strings.suffixFromNow = suffix
+ return $('body').data('page').split(':')[0] is 'projects'
- timefor = $.timeago time
- if timefor.indexOf('ago') > -1
- timefor = expiredLabel
+ w.gl.utils.getProjectSlug = ->
- jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
+ return if @isInProjectPage() then $('body').data 'project' else null
+
+
+ w.gl.utils.getGroupSlug = ->
+
+ return if @isInGroupsPage() then $('body').data 'group' else null
- return timefor
gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
@@ -32,6 +31,7 @@
.attr 'title', newTitle
.tooltip 'fixTitle'
+
gl.utils.preventDisabledButtons = ->
$('.btn').click (e) ->
@@ -40,4 +40,26 @@
e.stopImmediatePropagation()
return false
+
+ jQuery.timefor = (time, suffix, expiredLabel) ->
+
+ return '' unless time
+
+ suffix or= 'remaining'
+ expiredLabel or= 'Past due'
+
+ jQuery.timeago.settings.allowFuture = yes
+
+ { suffixFromNow } = jQuery.timeago.settings.strings
+ jQuery.timeago.settings.strings.suffixFromNow = suffix
+
+ timefor = $.timeago time
+
+ if timefor.indexOf('ago') > -1
+ timefor = expiredLabel
+
+ jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
+
+ return timefor
+
) window
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 1f46e331427..dabfd91cf14 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -9,7 +9,7 @@ class @MergeRequest
# Options:
# action - String, current controller action
#
- constructor: (@opts) ->
+ constructor: (@opts = {}) ->
this.$el = $('.merge-request')
this.$('.show-all-commits').on 'click', =>
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 49a4727205a..894f80586f1 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -88,7 +88,7 @@ class @MergeRequestTabs
scrollToElement: (container) ->
if window.location.hash
- navBarHeight = $('.navbar-gitlab').outerHeight()
+ navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
$el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index b108f747bd6..02480f3a025 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -24,14 +24,10 @@ class @MilestoneSelect
if issueUpdateURL
milestoneLinkTemplate = _.template(
- '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>">
- <span class="has-tooltip" data-container="body" title="<%= remaining %>">
- <%= _.escape(title) %>
- </span>
- </a>'
+ '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>" class="bold has-tooltip" data-container="body" title="<%= remaining %>"><%= _.escape(title) %></a>'
)
- milestoneLinkNoneTemplate = '<div class="light">None</div>'
+ milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
collapsedSidebarLabelTemplate = _.template(
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">
diff --git a/app/assets/javascripts/network/application.js.coffee b/app/assets/javascripts/network/application.js.coffee
new file mode 100644
index 00000000000..cb9eead855b
--- /dev/null
+++ b/app/assets/javascripts/network/application.js.coffee
@@ -0,0 +1,20 @@
+# This is a manifest file that'll be compiled into including all the files listed below.
+# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+# be included in the compiled file accessible from http://example.com/assets/application.js
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+#= require raphael
+#= require g.raphael
+#= require g.bar
+#= require_tree .
+
+$ ->
+ network_graph = new Network({
+ url: $(".network-graph").attr('data-url'),
+ commit_url: $(".network-graph").attr('data-commit-url'),
+ ref: $(".network-graph").attr('data-ref'),
+ commit_id: $(".network-graph").attr('data-commit-id')
+ })
+
+ new ShortcutsNetwork(network_graph.branch_graph)
diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/network/branch-graph.js.coffee
index f2fd2a775a4..f2fd2a775a4 100644
--- a/app/assets/javascripts/branch-graph.js.coffee
+++ b/app/assets/javascripts/network/branch-graph.js.coffee
diff --git a/app/assets/javascripts/network.js.coffee b/app/assets/javascripts/network/network.js.coffee
index f4ef07a50a7..f4ef07a50a7 100644
--- a/app/assets/javascripts/network.js.coffee
+++ b/app/assets/javascripts/network/network.js.coffee
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index 5eb915a51ea..421328554b8 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -67,8 +67,12 @@ class @SearchAutocomplete
getData: (term, callback) ->
_this = @
- # Do not trigger request if input is empty
- return if @searchInput.val() is ''
+ unless term
+ if contents = @getCategoryContents()
+ @searchInput.data('glDropdown').filter.options.callback contents
+ @enableAutocomplete()
+
+ return
# Prevent multiple ajax calls
return if @loadingSuggestions
@@ -122,6 +126,37 @@ class @SearchAutocomplete
).always ->
_this.loadingSuggestions = false
+
+ getCategoryContents: ->
+
+ userId = gon.current_user_id
+ { utils, projectOptions, groupOptions, dashboardOptions } = gl
+
+ if utils.isInGroupsPage() and groupOptions
+ options = groupOptions[utils.getGroupSlug()]
+
+ else if utils.isInProjectPage() and projectOptions
+ options = projectOptions[utils.getProjectSlug()]
+
+ else if dashboardOptions
+ options = dashboardOptions
+
+ { issuesPath, mrPath, name } = options
+
+ items = [
+ { header: "#{name}" }
+ { text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
+ { text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
+ 'separator'
+ { text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
+ { text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
+ ]
+
+ items.splice 0, 1 unless name
+
+ return items
+
+
serializeState: ->
{
# Search Criteria
@@ -209,6 +244,12 @@ class @SearchAutocomplete
@isFocused = true
@wrap.addClass('search-active')
+ @getData() if @getValue() is ''
+
+
+ getValue: -> return @searchInput.val()
+
+
onClearInputClick: (e) =>
e.preventDefault()
@searchInput.val('').focus()
@@ -229,6 +270,10 @@ class @SearchAutocomplete
@locationBadgeEl.text(badgeText).show()
@wrap.addClass('has-location-badge')
+
+ hasLocationBadge: -> return @wrap.is '.has-location-badge'
+
+
restoreOriginalState: ->
inputs = Object.keys @originalState
@@ -257,13 +302,14 @@ class @SearchAutocomplete
@getElement("##{input}").val('')
+
removeLocationBadge: ->
- @locationBadgeEl.hide()
- # Reset state
+ @locationBadgeEl.hide()
@resetSearchState()
-
@wrap.removeClass('has-location-badge')
+ @disableAutocomplete()
+
disableAutocomplete: ->
@searchInput.addClass('disabled')
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index f3d66004138..c03877e9b06 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -1,7 +1,7 @@
class @Shortcuts
- constructor: ->
+ constructor: (skipResetBindings) ->
@enabledHelp = []
- Mousetrap.reset()
+ Mousetrap.reset() if not skipResetBindings
Mousetrap.bind('?', @onToggleHelp)
Mousetrap.bind('s', Shortcuts.focusSearch)
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
diff --git a/app/assets/javascripts/shortcuts_blob.coffee b/app/assets/javascripts/shortcuts_blob.coffee
new file mode 100644
index 00000000000..6d21e5d1150
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_blob.coffee
@@ -0,0 +1,10 @@
+#= require shortcuts
+
+class @ShortcutsBlob extends Shortcuts
+ constructor: (skipResetBindings) ->
+ super skipResetBindings
+ Mousetrap.bind('y', ShortcutsBlob.copyToClipboard)
+
+ @copyToClipboard: ->
+ clipboardButton = $('.btn-clipboard')
+ clipboardButton.click() if clipboardButton
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index 2ce63c16428..68009e58645 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -3,13 +3,33 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
- $('header').toggleClass("header-collapsed header-expanded")
+ $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded")
+
+ if $.cookie('pin_nav') is 'true'
+ $('.navbar-fixed-top').toggleClass('header-pinned-nav')
+ $('.page-with-sidebar').toggleClass('page-sidebar-pinned')
setTimeout ( ->
- niceScrollBars = $('.nicescroll').niceScroll();
+ niceScrollBars = $('.nav-sidebar').niceScroll();
niceScrollBars.updateScrollBar();
), 300
+$(document)
+ .off 'click', 'body'
+ .on 'click', 'body', (e) ->
+ unless $.cookie('pin_nav') is 'true'
+ $target = $(e.target)
+ $nav = $target.closest('.sidebar-wrapper')
+ pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
+ $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle')
+
+ if $nav.length is 0 and pageExpanded and $toggle.length is 0
+ $('.page-with-sidebar')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+
+ $('.navbar-fixed-top')
+ .toggleClass('header-collapsed header-expanded')
+
$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
e.preventDefault()
diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee
index 26a26061539..c081f023b04 100644
--- a/app/assets/javascripts/users/calendar.js.coffee
+++ b/app/assets/javascripts/users/calendar.js.coffee
@@ -6,12 +6,6 @@ class @Calendar
@daySizeWithSpace = @daySize + (@daySpace * 2)
@monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
@months = []
- @highestValue = 0
-
- # Get the highest value from the timestampes
- _.each timestamps, (count) =>
- if count > @highestValue
- @highestValue = count
# Loop through the timestamps to create a group of objects
# The group of objects will be grouped based on the day of the week they are
@@ -39,8 +33,8 @@ class @Calendar
i++
# Init color functions
- @color = @initColor()
@colorKey = @initColorKey()
+ @color = @initColor()
# Init the svg element
@renderSvg(group)
@@ -104,7 +98,7 @@ class @Calendar
.attr 'class', 'user-contrib-cell js-tooltip'
.attr 'fill', (stamp) =>
if stamp.count isnt 0
- @color(stamp.count)
+ @color(Math.min(stamp.count, 40))
else
'#ededed'
.attr 'data-container', 'body'
@@ -164,10 +158,11 @@ class @Calendar
color
initColor: ->
+ colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
d3.scale
- .linear()
- .range(['#acd5f2', '#254e77'])
- .domain([0, @highestValue])
+ .threshold()
+ .domain([0, 10, 20, 30])
+ .range(colorRange)
initColorKey: ->
d3.scale
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 3dbc1d7f14f..2548efb2186 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -72,7 +72,7 @@ class @UsersSelect
assigneeTemplate = _.template(
'<% if (username) { %>
- <a class="author_link " href="/u/<%= username %>">
+ <a class="author_link bold" href="/u/<%= username %>">
<% if( avatar ) { %>
<img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>">
<% } %>
@@ -82,7 +82,7 @@ class @UsersSelect
</span>
</a>
<% } else { %>
- <span class="assign-yourself">
+ <span class="no-value assign-yourself">
No assignee -
<a href="#" class="js-assign-yourself">
assign yourself
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index fab96404a6c..d5fe5bc2ef1 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -91,6 +91,10 @@
background-color: $white-light;
border-top: none;
}
+
+ &.top-block .container-fluid {
+ background-color: inherit;
+ }
}
.cover-block {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 408d4a68e1e..0a8603b6702 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -8,8 +8,8 @@
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
-
- .collapse-nav a {
+ .toggle-nav-collapse,
+ .pin-nav-btn {
color: $color-light;
background: $color;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 63996ea44f6..a7bcb456560 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -2,8 +2,19 @@
* Application Header
*
*/
+@mixin tanuki-logo-colors($path-color) {
+ fill: $path-color;
+ transition: all 0.8s;
+
+ &:hover,
+ &.highlight {
+ fill: lighten($path-color, 25%);
+ transition: all 0.1s;
+ }
+}
+
header {
- transition-duration: .3s;
+ transition: padding $sidebar-transition-duration;
&.navbar-empty {
height: $header-height;
@@ -79,14 +90,9 @@ header {
&.header-collapsed {
padding: 0 16px;
-
- .side-nav-toggle {
- display: block;
- }
}
.side-nav-toggle {
- display: none;
position: absolute;
left: -10px;
margin: 6px 0;
@@ -108,9 +114,7 @@ header {
.header-content {
position: relative;
height: $header-height;
- padding-right: 40px;
padding-left: 30px;
- transition-duration: .3s;
@media (min-width: $screen-sm-min) {
padding-right: 0;
@@ -198,25 +202,24 @@ header {
}
}
-.header-collapsed {
- margin-left: 0;
+#tanuki-logo {
- .header-content {
-
- @media (min-width: $screen-sm-max) {
- padding-left: 30px;
- transition-duration: .3s;
- }
+ #tanuki-left-ear,
+ #tanuki-right-ear,
+ #tanuki-nose {
+ @include tanuki-logo-colors($tanuki-red);
}
-}
-.tanuki-shape {
- transition: all 0.8s;
+ #tanuki-left-eye,
+ #tanuki-right-eye {
+ @include tanuki-logo-colors($tanuki-orange);
+ }
- &:hover, &.highlight {
- fill: rgb(255, 255, 255);
- transition: all 0.1s;
+ #tanuki-left-cheek,
+ #tanuki-right-cheek {
+ @include tanuki-logo-colors($tanuki-yellow);
}
+
}
@media (max-width: $screen-xs-max) {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index b34ec16cdba..a12c0bba44a 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -159,7 +159,7 @@ ul.content-list {
background-color: $gray-light;
border: dotted 1px $gray-dark;
margin: 1px 0;
- min-height: 30px;
+ min-height: 52px;
}
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 829222509f0..a55918f8711 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -242,6 +242,12 @@
}
}
}
+
+ &.adjust {
+ .nav-text, .nav-controls {
+ width: auto;
+ }
+ }
}
.layout-nav {
@@ -251,7 +257,7 @@
z-index: 11;
background: $background-color;
border-bottom: 1px solid $border-color;
- transition-duration: .3s;
+ transition: padding $sidebar-transition-duration;
text-align: center;
.container-fluid {
@@ -347,6 +353,12 @@
.badge {
color: $gl-icon-color;
}
+
+ &:hover {
+ a, i {
+ color: $black;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 4668e7e911b..a0bb3427af0 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,26 +1,31 @@
.page-with-sidebar {
padding-top: $header-height;
- transition-duration: .3s;
+ transition: padding $sidebar-transition-duration;
.sidebar-wrapper {
position: fixed;
top: 0;
bottom: 0;
- overflow-y: auto;
- overflow-x: hidden;
left: 0;
height: 100%;
- transition-duration: .3s;
+ overflow: hidden;
+ transition: width $sidebar-transition-duration;
}
}
.sidebar-wrapper {
z-index: 1000;
background: $background-color;
+
+ .nicescroll-rails-hr {
+ // TODO: Figure out why nicescroll doesn't hide horizontal bar
+ display: none!important;
+ }
}
.content-wrapper {
width: 100%;
+ transition: padding $sidebar-transition-duration;
.container-fluid {
background: #fff;
@@ -34,50 +39,39 @@
}
}
-.sidebar-wrapper {
-
- .sidebar-user {
- padding: 15px 22px;
- position: fixed;
- bottom: 0;
- width: $sidebar_width;
- overflow: hidden;
- transition-duration: .3s;
+.sidebar-user {
+ padding: 15px;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: $sidebar_width;
+ overflow: hidden;
+ font-size: 16px;
+ line-height: 36px;
+ transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
- .username {
- margin-left: 10px;
- width: $sidebar_width - 2 * 10px;
- font-size: 16px;
- line-height: 34px;
- }
+ @media (min-width: $sidebar-breakpoint) {
+ bottom: 50px;
}
}
+.nav-sidebar {
+ position: absolute;
+ top: 50px;
+ bottom: 65px;
+ width: $sidebar_width;
+ overflow-y: auto;
+ overflow-x: hidden;
-.tanuki-shape {
- transition: all 0.8s;
-
- &:hover, &.highlight {
- fill: rgb(255, 255, 255);
- transition: all 0.1s;
+ @media (min-width: $sidebar-breakpoint) {
+ bottom: 115px;
}
-}
-
-
-.nav-sidebar {
- margin-top: 22 + $header-height;
- margin-bottom: 116px;
- transition-duration: .3s;
- list-style: none;
- overflow: hidden;
&.navbar-collapse {
padding: 0 !important;
}
li {
- width: $sidebar_width;
-
&.separate-item {
padding-top: 10px;
margin-top: 10px;
@@ -90,20 +84,18 @@
}
a {
- width: $sidebar_width;
- padding: 7px 15px 7px 23px;
+ padding: 7px 15px 7px 12px;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
outline: none;
+ white-space: nowrap;
- &:hover {
- text-decoration: none;
- }
-
- &:active, &:focus {
+ &:hover,
+ &:active,
+ &:focus {
text-decoration: none;
}
@@ -115,10 +107,6 @@
svg {
margin-right: 13px;
}
-
- &.back-link i {
- transition-duration: .3s;
- }
}
}
@@ -129,37 +117,50 @@
}
}
-.sidebar-subnav {
- margin-left: 0;
- padding-left: 0;
-
- li {
- list-style: none;
- }
-}
-
-.collapse-nav a {
+.toggle-nav-collapse {
width: $sidebar_width;
- position: fixed;
+ position: absolute;
top: 0;
left: 0;
+ min-height: 50px;
padding: 5px 0;
font-size: 18px;
- background: transparent;
- height: 50px;
- text-align: center;
- line-height: 40px;
+ line-height: 30px;
+}
+
+.nav-header-btn {
+ padding: 10px 5px;
+ color: inherit;
transition-duration: .3s;
- outline: none;
- &:hover {
+ &:hover,
+ &:focus {
+ color: $white-light;
text-decoration: none;
}
}
-.sidebar-wrapper {
- &.hidden-nav {
- width: 0;
+.pin-nav-btn {
+ display: none;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ height: 50px;
+ width: $sidebar_width;
+ line-height: 30px;
+
+ @media (min-width: $sidebar-breakpoint) {
+ display: block;
+ }
+
+ .fa {
+ transition: transform .15s;
+ }
+
+ &.is-active {
+ .fa {
+ transform: rotate(90deg);
+ }
}
}
@@ -168,62 +169,34 @@
.sidebar-wrapper {
width: 0;
-
- .nav-sidebar {
- width: 0;
-
- li {
- width: auto;
-
- a {
- span {
- display: none;
- }
- }
- }
- }
-
- .collapse-nav a {
- width: 0;
-
- i {
- display: none;
- }
- }
-
- .sidebar-user {
- width: 0;
- padding-left: 0;
- padding-right: 0;
-
- .username {
- display: none;
- }
- }
}
}
.page-sidebar-expanded {
-
- @media (max-width: $screen-sm-max) {
- padding-left: 0;
- }
-
.sidebar-wrapper {
width: $sidebar_width;
+ }
+}
- .nav-sidebar {
- width: $sidebar_width;
+.page-sidebar-pinned {
+ .content-wrapper,
+ .layout-nav {
+ @media (min-width: $sidebar-breakpoint) {
+ padding-left: $sidebar_width;
}
+ }
+}
- .nav-sidebar li a {
- width: $sidebar_width;
+header.header-pinned-nav {
+ @media (min-width: $sidebar-breakpoint) {
+ padding-left: ($sidebar_width + $gl-padding);
- &.back-link {
- i {
- opacity: 0;
- }
- }
+ .side-nav-toggle {
+ display: none;
+ }
+
+ .header-content {
+ padding-left: 0;
}
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 752d8ec8788..c37574ca7a1 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -6,6 +6,8 @@ $sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 258px;
+$sidebar-transition-duration: .15s;
+$sidebar-breakpoint: 1440px;
/*
* UI elements
@@ -154,6 +156,11 @@ $warning-message-border: #f0e2bb;
/* header */
$light-grey-header: #faf9f9;
+/* tanuki logo colors */
+$tanuki-red: #e24329;
+$tanuki-orange: #fc6d26;
+$tanuki-yellow: #fca326;
+
/*
* State colors:
*/
@@ -261,5 +268,10 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
+/*
+ * Personal Access Tokens
+ */
+$personal-access-tokens-disabled-label-color: #bbb;
+
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss
index 28611a5ec81..9495c5b3f37 100644
--- a/app/assets/stylesheets/mailers/devise.scss
+++ b/app/assets/stylesheets/mailers/devise.scss
@@ -38,6 +38,10 @@ table {
margin: 0 auto;
text-align: left;
width: 600px;
+
+ & > td {
+ text-align: center;
+ }
}
&#body {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c8c6bbde084..761e33f0df7 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -7,84 +7,111 @@
margin-right: 9px;
}
-.lists-separator {
- margin: 10px 0;
- border-color: #ddd;
+.commit-header {
+ padding: 5px 10px;
+ background-color: $background-color;
+ border-top: 1px solid #eee;
+ border-bottom: 1px solid #eee;
+ font-size: 14px;
+
+ &:first-child {
+ border-top-width: 0;
+ }
}
-.commits-row {
- ul {
- margin: 0;
+.commit-row-title {
+ line-height: 1;
+ margin-bottom: 7px;
- li.commit {
- padding: 8px 0;
- }
+ .notes_count {
+ float: right;
+ margin-right: 10px;
+ }
+
+ .str-truncated {
+ max-width: 70%;
}
- .commits-row-date {
- font-size: 15px;
- line-height: 20px;
- margin-bottom: 5px;
+ .commit-row-message {
+ color: $gl-dark-link-color;
+ }
+
+ .text-expander {
+ display: inline-block;
+ background: $gray-light;
+ color: $gl-placeholder-color;
+ padding: 0 5px;
+ cursor: pointer;
+ border: 1px solid $border-gray-dark;
+ border-radius: $border-radius-default;
+ margin-left: 5px;
+
+ &:hover {
+ background-color: darken($gray-light, 10%);
+ text-decoration: none;
+ }
}
}
-li.commit {
- list-style: none;
+.commit-actions {
+ @media (min-width: $screen-sm-min) {
+ float: right;
+ margin-left: $gl-padding;
+ margin-top: 2px;
+ font-size: 0;
+ }
- .commit-row-title {
- font-size: $list-font-size;
- line-height: 20px;
- margin-bottom: 2px;
+ .btn-transparent {
+ padding-left: 0;
+ padding-right: 0;
+ }
- .btn-clipboard {
- margin-top: -1px;
+ .btn {
+ &:not(:first-child) {
+ margin-left: $gl-padding;
}
+ }
+}
- .notes_count {
- float: right;
- margin-right: 10px;
- }
+.commit-short-id {
+ font-family: $monospace_font;
+ font-weight: 600;
+}
- .commit_short_id {
- min-width: 65px;
- color: $gl-dark-link-color;
- font-family: $monospace_font;
- }
+.commit {
+ padding: 10px 0;
- .str-truncated {
- max-width: 70%;
- }
+ @media (min-width: $screen-sm-min) {
+ padding-left: 46px;
+ }
- .commit-row-message {
- color: $gl-dark-link-color;
+ &:not(:last-child) {
+ border-bottom: 1px solid #eee;
+ }
- &:hover {
- text-decoration: underline;
- }
- }
+ a,
+ button {
+ color: $gl-dark-link-color;
+ vertical-align: baseline;
+ }
- .text-expander {
- background: #eee;
- color: #555;
- padding: 0 5px;
- cursor: pointer;
- margin-left: 4px;
- &:hover {
- background-color: #ddd;
- }
- }
+ .avatar {
+ margin-left: -46px;
}
.item-title {
display: inline-block;
- max-width: 70%;
+
+ @media (min-width: $screen-sm-min) {
+ max-width: 70%;
+ }
}
.commit-row-description {
font-size: 14px;
border-left: 1px solid #eee;
padding: 10px 15px;
- margin: 5px 0 10px 5px;
+ margin: 10px 0;
background: #f9f9f9;
display: none;
@@ -93,6 +120,7 @@ li.commit {
background: inherit;
padding: 0;
margin: 0;
+ white-space: pre-wrap;
}
a {
@@ -102,7 +130,7 @@ li.commit {
.commit-row-info {
color: $gl-gray;
- line-height: 24px;
+ line-height: 1;
a {
color: $gl-gray;
@@ -111,10 +139,6 @@ li.commit {
.avatar {
margin-right: 8px;
}
-
- .committed_ago {
- display: inline-block;
- }
}
&.inline-commit {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 22679c764dc..a34b06f1054 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -66,8 +66,7 @@
font-family: $regular_font;
}
- .gitignore-selector {
-
+ .gitignore-selector, .license-selector {
.dropdown {
line-height: 21px;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index f57845ad9c9..687117233f6 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -145,7 +145,6 @@
.assign-yourself {
margin-top: 10px;
- font-weight: normal;
display: block;
}
}
@@ -158,6 +157,10 @@
font-weight: normal;
}
+ .no-value {
+ color: $gl-placeholder-color;
+ }
+
.sidebar-collapsed-icon {
display: none;
}
@@ -248,11 +251,16 @@
padding-bottom: 0;
margin-bottom: 10px;
}
+
+ .issuable-header-btn {
+ display: none;
+ }
}
.issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
+
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
@@ -322,7 +330,7 @@
margin-left: 5px;
a {
- color: #8c8c8c;
+ color: $gl-placeholder-color;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index bc65404a741..046c38aba44 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -115,6 +115,13 @@
}
}
+.draggable-handler {
+ display: inline-block;
+ opacity: 0;
+ transition: opacity .3s;
+ color: $gray-darkest;
+}
+
.prioritized-labels {
margin-bottom: 30px;
@@ -122,6 +129,13 @@
display: none;
color: $gray-light;
}
+
+ li:hover {
+ .draggable-handler {
+ display: inline-block;
+ opacity: 1;
+ }
+ }
}
.other-labels {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 53bff508c72..e67271adfb1 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -244,6 +244,10 @@
.panel-footer {
padding: 5px 10px;
+
+ .btn {
+ min-width: auto;
+ }
}
.commit {
@@ -252,9 +256,7 @@
}
.avatar {
- width: 20px;
- height: 20px;
- margin-right: 5px;
+ margin-left: 0;
}
.commit-row-info {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 0c084118753..35d728aec83 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -139,6 +139,12 @@ ul.notes {
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
+
+ @media (max-width: $screen-xs-min) {
+ .inline {
+ display: block;
+ }
+ }
}
.note-emoji-button {
@@ -258,7 +264,11 @@ ul.notes {
position: absolute;
right: 0;
top: 0;
-
+
+ .note-action-button {
+ margin-left: 10px;
+ }
+
@media (min-width: $screen-sm-min) {
position: relative;
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 167ab40d881..46371ec6871 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -192,6 +192,25 @@
}
}
+.personal-access-tokens-never-expires-label {
+ color: $personal-access-tokens-disabled-label-color;
+}
+
+.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
+ text-align: center;
+}
+
+.created-personal-access-token-container {
+ #created-personal-access-token {
+ width: 90%;
+ display: inline;
+ }
+
+ .btn-clipboard {
+ margin-left: 5px;
+ }
+}
+
.user-profile {
@media (max-width: $screen-xs-max) {
.cover-block {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0e4cefc55c2..855d86cb238 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -5,10 +5,12 @@
font-weight: normal;
}
}
+
.no-ssh-key-message, .project-limit-message {
background-color: #f28d35;
margin-bottom: 0;
}
+
.new_project,
.edit-project {
fieldset.features {
@@ -18,13 +20,6 @@
}
}
-.project-name-holder {
- .help-inline {
- vertical-align: top;
- padding: 7px;
- }
-}
-
.project-home-panel {
background: $white-light;
text-align: left;
@@ -33,7 +28,7 @@
.container-fluid {
position: relative;
- @media (min-width: $screen-md-max) {
+ @media (min-width: $screen-lg-min) {
.row {
display: flex;
-ms-flex-align: center;
@@ -229,7 +224,7 @@
right: 16px;
bottom: 0;
- @media (max-width: $screen-lg-min) {
+ @media (max-width: $screen-md-max) {
top: 0;
}
@@ -238,7 +233,7 @@
right: 0;
bottom: 61px;
- @media (max-width: $screen-lg-min) {
+ @media (max-width: $screen-md-max) {
position: relative;
bottom: 0;
margin-right: 10px;
@@ -376,6 +371,7 @@ a.deploy-project-label {
.project-import .btn {
float: left;
+ margin-bottom: 10px;
margin-right: 10px;
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index f16fc7f388f..99c9e81ddb9 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -101,7 +101,7 @@
margin: 0;
.commit {
- padding: 0;
+ padding: 0 0 0 55px;
.commit-row-title {
.commit-row-message {
@@ -129,4 +129,6 @@
.tree-controls {
float: right;
margin-top: 11px;
+ position: relative;
+ z-index: 2;
}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index cd6ae507cf1..dd1bc6f5d52 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -8,7 +8,7 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper
include WorkhorseHelper
- before_action :authenticate_user_from_token!
+ before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
before_action :reject_blocked!
@@ -24,7 +24,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings
- helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
+ helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -64,17 +64,10 @@ class ApplicationController < ActionController::Base
end
end
- # From https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
- # https://gist.github.com/josevalim/fb706b1e933ef01e4fb6
- def authenticate_user_from_token!
- user_token = if params[:authenticity_token].presence
- params[:authenticity_token].presence
- elsif params[:private_token].presence
- params[:private_token].presence
- elsif request.headers['PRIVATE-TOKEN'].present?
- request.headers['PRIVATE-TOKEN']
- end
- user = user_token && User.find_by_authentication_token(user_token.to_s)
+ # This filter handles both private tokens and personal access tokens
+ def authenticate_user_from_private_token!
+ token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
+ user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
if user
# Notice we are passing store false, so the user is not
@@ -326,6 +319,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('git')
end
+ def gitlab_project_import_enabled?
+ current_application_settings.import_sources.include?('gitlab_project')
+ end
+
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index f9a1929c117..7842fb9ce63 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def destroy
- todo.done
+ TodoService.new.mark_todos_as_done([todo], current_user)
todo_notice = 'Todo was successfully marked as done.'
@@ -14,20 +14,20 @@ class Dashboard::TodosController < Dashboard::ApplicationController
format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.js { head :ok }
format.json do
- render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ render json: { count: @todos.size, done_count: current_user.todos_done_count }
end
end
end
def destroy_all
- @todos.each(&:done)
+ TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
format.json do
find_todos
- render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ render json: { count: @todos.size, done_count: current_user.todos_done_count }
end
end
end
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
new file mode 100644
index 00000000000..f99aa490d3e
--- /dev/null
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -0,0 +1,48 @@
+class Import::GitlabProjectsController < Import::BaseController
+ before_action :verify_gitlab_project_import_enabled
+
+ def new
+ @namespace_id = project_params[:namespace_id]
+ @namespace_name = Namespace.find(project_params[:namespace_id]).name
+ @path = project_params[:path]
+ end
+
+ def create
+ unless file_is_valid?
+ return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
+ end
+
+ @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
+ current_user,
+ File.expand_path(project_params[:file].path),
+ project_params[:path]).execute
+
+ if @project.saved?
+ redirect_to(
+ project_path(@project),
+ notice: "Project '#{@project.name}' is being imported."
+ )
+ else
+ redirect_to(
+ new_import_gitlab_project_path,
+ alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}"
+ )
+ end
+ end
+
+ private
+
+ def file_is_valid?
+ project_params[:file] && project_params[:file].respond_to?(:read)
+ end
+
+ def verify_gitlab_project_import_enabled
+ render_404 unless gitlab_project_import_enabled?
+ end
+
+ def project_params
+ params.permit(
+ :path, :namespace_id, :file
+ )
+ end
+end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
new file mode 100644
index 00000000000..508b82a9a6c
--- /dev/null
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -0,0 +1,42 @@
+class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
+ before_action :load_personal_access_tokens, only: :index
+
+ def index
+ @personal_access_token = current_user.personal_access_tokens.build
+ end
+
+ def create
+ @personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
+
+ if @personal_access_token.save
+ flash[:personal_access_token] = @personal_access_token.token
+ redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
+ else
+ load_personal_access_tokens
+ render :index
+ end
+ end
+
+ def revoke
+ @personal_access_token = current_user.personal_access_tokens.find(params[:id])
+
+ if @personal_access_token.revoke!
+ flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
+ else
+ flash[:alert] = "Could not revoke personal access token #{@personal_access_token.name}."
+ end
+
+ redirect_to profile_personal_access_tokens_path
+ end
+
+ private
+
+ def personal_access_token_params
+ params.require(:personal_access_token).permit(:name, :expires_at)
+ end
+
+ def load_personal_access_tokens
+ @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
+ @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 67e7187c10d..851822d805a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -204,10 +204,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.update(merge_error: nil)
- if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active?
- MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
- .execute(@merge_request)
- @status = :merge_when_build_succeeds
+ if params[:merge_when_build_succeeds].present?
+ if @merge_request.pipeline && @merge_request.pipeline.active?
+ MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
+ .execute(@merge_request)
+ @status = :merge_when_build_succeeds
+ elsif @merge_request.pipeline.success?
+ # This can be triggered when a user clicks the auto merge button while
+ # the tests finish at about the same time
+ MergeWorker.perform_async(@merge_request.id, current_user.id, params)
+ @status = :success
+ else
+ @status = :failed
+ end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 46b242aa5ff..6dc495247c8 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -6,8 +6,10 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def index
- sorted = VersionSorter.rsort(@repository.tag_names)
- @tags = Kaminari.paginate_array(sorted).page(params[:page])
+ @sort = params[:sort] || 'name'
+ @tags = @repository.tags_sorted_by(@sort)
+ @tags = Kaminari.paginate_array(@tags).page(params[:page])
+
@releases = project.releases.where(tag: @tags)
end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index a51bd5e2b49..648d42c56c5 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -4,7 +4,7 @@ class Projects::TodosController < Projects::ApplicationController
render json: {
todo: todos,
- count: current_user.todos.pending.count,
+ count: current_user.todos_pending_count,
}
end
@@ -12,7 +12,7 @@ class Projects::TodosController < Projects::ApplicationController
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: {
- count: current_user.todos.pending.count,
+ count: current_user.todos_pending_count,
}
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a6479c42d94..8044c637825 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -7,7 +7,7 @@ class ProjectsController < Projects::ApplicationController
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping]
+ before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
@@ -143,6 +143,7 @@ class ProjectsController < Projects::ApplicationController
issues: autocomplete.issues,
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
+ labels: autocomplete.labels,
members: participants
}
@@ -185,6 +186,48 @@ class ProjectsController < Projects::ApplicationController
)
end
+ def export
+ @project.add_export_job(current_user: current_user)
+
+ redirect_to(
+ edit_project_path(@project),
+ notice: "Project export started. A download link will be sent by email."
+ )
+ end
+
+ def download_export
+ export_project_path = @project.export_project_path
+
+ if export_project_path
+ send_file export_project_path, disposition: 'attachment'
+ else
+ redirect_to(
+ edit_project_path(@project),
+ alert: "Project export link has expired. Please generate a new export from your project settings."
+ )
+ end
+ end
+
+ def remove_export
+ if @project.remove_exports
+ flash[:notice] = "Project export has been deleted."
+ else
+ flash[:alert] = "Project export could not be deleted."
+ end
+ redirect_to(edit_project_path(@project))
+ end
+
+ def generate_new_export
+ if @project.remove_exports
+ export
+ else
+ redirect_to(
+ edit_project_path(@project),
+ alert: "Project export could not be deleted."
+ )
+ end
+ end
+
def toggle_star
current_user.toggle_star(@project)
@project.reload
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 85559fbc5f5..5b54b34070c 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -180,8 +180,8 @@ module BlobHelper
licenses = Licensee::License.all
@licenses_for_select = {
- Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
- Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
+ Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
+ Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
}
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index f742922d926..9051a493b9b 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -17,7 +17,25 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
- class: 'btn btn-clipboard',
+ class: "btn btn-clipboard",
+ data: data,
+ type: :button
+ end
+
+ # Output a "Copy to Clipboard" button with a custom CSS class
+ #
+ # data - Data attributes passed to `content_tag`
+ # css_class - Class passed to the `content_tag`
+ #
+ # Examples:
+ #
+ # # Define the target element
+ # clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard")
+ # # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>"
+ def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
+ content_tag :button,
+ icon('clipboard'),
+ class: "btn #{css_class}",
data: data,
type: :button
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 07e5c146844..8e4ae1e6aec 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -38,10 +38,10 @@ module CiStatusHelper
icon(icon_name + ' fw')
end
- def render_commit_status(commit, tooltip_placement: 'auto left')
+ def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
- render_status_with_link('commit', commit.status, path, tooltip_placement)
+ render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
@@ -57,10 +57,10 @@ module CiStatusHelper
private
- def render_status_with_link(type, status, path, tooltip_placement)
+ def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
link_to ci_icon_for_status(status),
path,
- class: "ci-status-link ci-status-icon-#{status.dasherize}",
+ class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
title: "#{type.titleize}: #{ci_label_for_status(status)}",
data: { toggle: 'tooltip', placement: tooltip_placement }
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 493505e0c95..474041eccbb 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -16,6 +16,16 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
+ def commit_author_avatar(commit, options = {})
+ options = options.merge(source: :author)
+ user = commit.send(options[:source])
+
+ source_email = clean(commit.send "#{options[:source]}_email".to_sym)
+ person_email = user.try(:email) || source_email
+
+ image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
+ end
+
def image_diff_class(diff)
if diff.deleted_file
"deleted"
@@ -102,24 +112,24 @@ module CommitsHelper
if current_controller?(:projects, :commits)
if @repo.blob_at(commit.id, @path)
return link_to(
- "Browse File »",
+ "Browse File",
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
- class: "pull-right"
+ class: "btn btn-default"
)
elsif @path.present?
return link_to(
- "Browse Directory »",
+ "Browse Directory",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
- class: "pull-right"
+ class: "btn btn-default"
)
end
end
link_to(
"Browse Files",
namespace_project_tree_path(project.namespace, project, commit),
- class: "pull-right"
+ class: "btn btn-default"
)
end
@@ -187,12 +197,10 @@ module CommitsHelper
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
person_name = user.try(:name) || source_name
- person_email = user.try(:email) || source_email
text =
if options[:avatar]
- avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "")
- %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>}
+ %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
else
person_name
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index cbe47176831..e22dce59d0f 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -135,6 +135,11 @@ module DiffHelper
toggle_whitespace_link(url, options)
end
+ def diff_compare_whitespace_link(project, from, to, options)
+ url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace)
+ toggle_whitespace_link(url, options)
+ end
+
private
def hide_whitespace?
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index a53828ef4e7..877c77050be 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -6,12 +6,6 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym
end
- def can_see_member_roles?(source:, user: nil)
- return false unless user
-
- user.is_admin? || source.members.exists?(user_id: user.id)
- end
-
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 469accf3142..3ff8be5e284 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -12,10 +12,10 @@ module NavHelper
end
def page_sidebar_class
- if nav_menu_collapsed?
- "page-sidebar-collapsed"
+ if pinned_nav?
+ "page-sidebar-expanded page-sidebar-pinned"
else
- "page-sidebar-expanded"
+ "page-sidebar-collapsed"
end
end
@@ -36,7 +36,15 @@ module NavHelper
end
def nav_header_class
- class_name = " with-horizontal-nav" if defined?(nav) && nav
+ class_name = ''
+ class_name << " with-horizontal-nav" if defined?(nav) && nav
+
+ if pinned_nav?
+ class_name << " header-expanded header-pinned-nav"
+ else
+ class_name << " header-collapsed"
+ end
+
class_name
end
@@ -47,4 +55,8 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
+
+ def pinned_nav?
+ cookies[:pin_nav] == 'true'
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 3b4e431a491..d91e3332e48 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -41,7 +41,7 @@ module ProjectsHelper
author_html = author_html.html_safe
if opts[:name]
- link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
+ link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 9adf5ef29f7..c7aeed4b9fc 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,10 +1,10 @@
module TodosHelper
def todos_pending_count
- current_user.todos.pending.count
+ current_user.todos_pending_count
end
def todos_done_count
- current_user.todos.done.count
+ current_user.todos_done_count
end
def todo_action_name(todo)
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 689fb3e0ffb..e0af7081411 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -9,6 +9,19 @@ module Emails
subject: subject("Project was moved"))
end
+ def project_was_exported_email(current_user, project)
+ @project = project
+ mail(to: current_user.notification_email,
+ subject: subject("Project was exported"))
+ end
+
+ def project_was_not_exported_email(current_user, project, errors)
+ @project = project
+ @errors = errors
+ mail(to: current_user.notification_email,
+ subject: subject("Project export error"))
+ end
+
def repository_push_email(project_id, opts = {})
@message =
Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a744f937918..d914b0b26eb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -123,7 +123,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
- import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
+ import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 764d8e4e136..d618c84e983 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -341,6 +341,7 @@ module Ci
def erase_artifacts!
remove_artifacts_file!
remove_artifacts_metadata!
+ save
end
def erase(opts = {})
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 4bbfb4cc806..5b264ecffc5 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -94,10 +94,13 @@ module Ci
end
def create_builds(user, trigger_request = nil)
+ ##
+ # We persist pipeline only if there are builds available
+ #
return unless config_processor
- config_processor.stages.any? do |stage|
- CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present?
- end
+
+ build_builds_for_stages(config_processor.stages, user,
+ 'success', trigger_request) && save
end
def create_next_builds(build)
@@ -115,10 +118,10 @@ module Ci
prior_builds = latest_builds.where.not(stage: next_stages)
prior_status = prior_builds.status
- # create builds for next stages based
- next_stages.any? do |stage|
- CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present?
- end
+ # build builds for next stage that has builds available
+ # and save pipeline if we have builds
+ build_builds_for_stages(next_stages, build.user, prior_status,
+ build.trigger_request) && save
end
def retried
@@ -139,10 +142,10 @@ module Ci
@config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
- save_yaml_error(e.message)
+ self.yaml_errors = e.message
nil
rescue
- save_yaml_error("Undefined error")
+ self.yaml_errors = 'Undefined error'
nil
end
end
@@ -167,8 +170,23 @@ module Ci
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
+ def notes
+ Note.for_commit_id(sha)
+ end
+
private
+ def build_builds_for_stages(stages, user, status, trigger_request)
+ ##
+ # Note that `Array#any?` implements a short circuit evaluation, so we
+ # build builds only for the first stage that has builds available.
+ #
+ stages.any? do |stage|
+ CreateBuildsService.new(self)
+ .execute(stage, user, status, trigger_request).present?
+ end
+ end
+
def update_state
statuses.reload
self.status = if yaml_errors.blank?
@@ -181,11 +199,5 @@ module Ci
self.duration = statuses.latest.duration
save
end
-
- def save_yaml_error(error)
- return if self.yaml_errors?
- self.yaml_errors = error
- update_state
- end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e53c483b904..ab13db4b297 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,5 +1,6 @@
class CommitStatus < ActiveRecord::Base
include Statuseable
+ include Importable
self.table_name = 'ci_builds'
@@ -7,7 +8,7 @@ class CommitStatus < ActiveRecord::Base
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user
- validates :pipeline, presence: true
+ validates :pipeline, presence: true, unless: :importing?
validates_presence_of :name
diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb
new file mode 100644
index 00000000000..019ef755849
--- /dev/null
+++ b/app/models/concerns/importable.rb
@@ -0,0 +1,6 @@
+module Importable
+ extend ActiveSupport::Concern
+
+ attr_accessor :importing
+ alias_method :importing?, :importing
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index b8dffe9f5b9..e66e04371b2 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -9,6 +9,12 @@ class Group < Namespace
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
+
+ has_many :owners,
+ -> { where(members: { access_level: Gitlab::Access::OWNER }) },
+ through: :group_members,
+ source: :user
+
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source
@@ -88,10 +94,6 @@ class Group < Namespace
end
end
- def owners
- @owners ||= group_members.owners.includes(:user).map(&:user)
- end
-
def add_users(user_ids, access_level, current_user = nil)
user_ids.each do |user_id|
Member.add_user(self.group_members, user_id, access_level, current_user)
diff --git a/app/models/jira_issue.rb b/app/models/jira_issue.rb
deleted file mode 100644
index 5b21aac5e43..00000000000
--- a/app/models/jira_issue.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class JiraIssue < ExternalIssue
-end
diff --git a/app/models/member.rb b/app/models/member.rb
index cea6d259760..4ee3f1bb5c2 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,5 +1,6 @@
class Member < ActiveRecord::Base
include Sortable
+ include Importable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -41,11 +42,11 @@ class Member < ActiveRecord::Base
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
- after_create :send_invite, if: :invite?
- after_create :send_request, if: :request?
- after_create :create_notification_setting, unless: :pending?
- after_create :post_create_hook, unless: :pending?
- after_update :post_update_hook, unless: :pending?
+ after_create :send_invite, if: :invite?, unless: :importing?
+ after_create :send_request, if: :request?, unless: :importing?
+ after_create :create_notification_setting, unless: [:pending?, :importing?]
+ after_create :post_create_hook, unless: [:pending?, :importing?]
+ after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
after_destroy :post_decline_request, if: :request?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7b8858b24d6..73bf182ec9f 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -4,6 +4,7 @@ class MergeRequest < ActiveRecord::Base
include Referable
include Sortable
include Taskable
+ include Importable
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
@@ -13,7 +14,7 @@ class MergeRequest < ActiveRecord::Base
serialize :merge_params, Hash
- after_create :create_merge_request_diff
+ after_create :create_merge_request_diff, unless: :importing
after_update :update_merge_request_diff
delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
@@ -95,12 +96,12 @@ class MergeRequest < ActiveRecord::Base
end
end
- validates :source_project, presence: true, unless: :allow_broken
+ validates :source_project, presence: true, unless: [:allow_broken, :importing?]
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
- validate :validate_branches, unless: :allow_broken
+ validate :validate_branches, unless: [:allow_broken, :importing?]
validate :validate_fork
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 7d5103748f5..aca377cc600 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,5 +1,6 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
+ include Importable
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
@@ -22,7 +23,7 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
- after_create :reload_content
+ after_create :reload_content, unless: :importing?
def reload_content
reload_commits
diff --git a/app/models/note.rb b/app/models/note.rb
index 4b6748053ff..8d164647550 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -4,6 +4,7 @@ class Note < ActiveRecord::Base
include Participable
include Mentionable
include Awardable
+ include Importable
default_value_for :system, false
@@ -28,11 +29,11 @@ class Note < ActiveRecord::Base
validates :attachment, file_size: { maximum: :max_attachment_size }
validates :noteable_type, presence: true
- validates :noteable_id, presence: true, unless: :for_commit?
+ validates :noteable_id, presence: true, unless: [:for_commit?, :importing?]
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
- validate unless: :for_commit? do |note|
+ validate unless: [:for_commit?, :importing?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
new file mode 100644
index 00000000000..c4b095e0c04
--- /dev/null
+++ b/app/models/personal_access_token.rb
@@ -0,0 +1,20 @@
+class PersonalAccessToken < ActiveRecord::Base
+ include TokenAuthenticatable
+ add_authentication_token_field :token
+
+ belongs_to :user
+
+ scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
+ scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
+
+ def self.generate(params)
+ personal_access_token = self.new(params)
+ personal_access_token.ensure_token
+ personal_access_token
+ end
+
+ def revoke!
+ self.revoked = true
+ self.save
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index fdbc84474ed..ca3bc04e2dd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -81,7 +81,7 @@ class Project < ActiveRecord::Base
has_one :jira_service, dependent: :destroy
has_one :redmine_service, dependent: :destroy
has_one :custom_issue_tracker_service, dependent: :destroy
- has_one :gitlab_issue_tracker_service, dependent: :destroy
+ has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
@@ -262,7 +262,23 @@ class Project < ActiveRecord::Base
#
# Returns a Project, or nil if no project could be found.
def find_with_namespace(path)
- where_paths_in([path]).reorder(nil).take
+ namespace_path, project_path = path.split('/', 2)
+
+ return unless namespace_path && project_path
+
+ namespace_path = connection.quote(namespace_path)
+ project_path = connection.quote(project_path)
+
+ # On MySQL we want to ensure the ORDER BY uses a case-sensitive match so
+ # any literal matches come first, for this we have to use "BINARY".
+ # Without this there's still no guarantee in what order MySQL will return
+ # rows.
+ binary = Gitlab::Database.mysql? ? 'BINARY' : ''
+
+ order_sql = "(CASE WHEN #{binary} namespaces.path = #{namespace_path} " \
+ "AND #{binary} projects.path = #{project_path} THEN 0 ELSE 1 END)"
+
+ where_paths_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple projects by their full paths.
@@ -351,6 +367,11 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC')
end
+
+ # Deletes gitlab project export files older than 24 hours
+ def remove_gitlab_exports!
+ Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete))
+ end
end
def team
@@ -454,7 +475,7 @@ class Project < ActiveRecord::Base
end
def import?
- external_import? || forked?
+ external_import? || forked? || gitlab_project_import?
end
def no_import?
@@ -485,6 +506,10 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new(import_url).masked_url
end
+ def gitlab_project_import?
+ import_type == 'gitlab_project'
+ end
+
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
projects_limit = creator.projects_limit
@@ -1080,4 +1105,27 @@ class Project < ActiveRecord::Base
ensure
@errors = original_errors
end
+
+ def add_export_job(current_user:)
+ job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
+
+ if job_id
+ Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
+ else
+ Rails.logger.error "Export job failed to start for project ID #{self.id}"
+ end
+ end
+
+ def export_path
+ File.join(Gitlab::ImportExport.storage_path, path_with_namespace)
+ end
+
+ def export_project_path
+ Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
+ end
+
+ def remove_exports
+ _, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
+ status.zero?
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e5b277cb198..bbd7682d8e7 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -243,7 +243,7 @@ class Repository
end
def cache_keys
- %i(size branch_names tag_names commit_count
+ %i(size branch_names tag_names branch_count tag_count commit_count
readme version contribution_guide changelog
license_blob license_key gitignore)
end
@@ -598,6 +598,21 @@ class Repository
end
end
+ def tags_sorted_by(value)
+ case value
+ when 'name'
+ # Would be better to use `sort_by` but `version_sorter` only exposes
+ # `sort` and `rsort`
+ VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) }
+ when 'updated_desc'
+ tags_sorted_by_committed_date.reverse
+ when 'updated_asc'
+ tags_sorted_by_committed_date
+ else
+ tags
+ end
+ end
+
def contributors
commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true)
@@ -995,4 +1010,8 @@ class Repository
def file_on_head(regex)
tree(:head).blobs.find { |file| file.name =~ regex }
end
+
+ def tags_sorted_by_committed_date
+ tags.sort_by { |tag| commit(tag.target).committed_date }
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index bf352397509..40d39933ad8 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -18,7 +18,7 @@ class Service < ActiveRecord::Base
after_commit :reset_updated_properties
after_commit :cache_project_has_external_issue_tracker
- belongs_to :project
+ belongs_to :project, inverse_of: :services
has_one :service_hook
validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
diff --git a/app/models/user.rb b/app/models/user.rb
index 8d0427da5ab..2e458329cb9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -51,6 +51,7 @@ class User < ActiveRecord::Base
# Profile
has_many :keys, dependent: :destroy
has_many :emails, dependent: :destroy
+ has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
@@ -267,6 +268,11 @@ class User < ActiveRecord::Base
find_by!('lower(username) = ?', username.downcase)
end
+ def find_by_personal_access_token(token_string)
+ personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
+ personal_access_token.user if personal_access_token
+ end
+
def by_username_or_id(name_or_id)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
@@ -821,6 +827,23 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true)
end
+ def todos_done_count(force: false)
+ Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
+ todos.done.count
+ end
+ end
+
+ def todos_pending_count(force: false)
+ Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
+ todos.pending.count
+ end
+ end
+
+ def update_todos_count_cache
+ todos_done_count(force: true)
+ todos_pending_count(force: true)
+ end
+
private
def projects_union(min_access_level = nil)
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 3a74ae094e8..2dcb052d274 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -2,10 +2,11 @@ module Ci
class CreateBuildsService
def initialize(pipeline)
@pipeline = pipeline
+ @config = pipeline.config_processor
end
def execute(stage, user, status, trigger_request = nil)
- builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
+ builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
# check when to create next build
builds_attrs = builds_attrs.select do |build_attrs|
@@ -19,34 +20,37 @@ module Ci
end
end
+ # don't create the same build twice
+ builds_attrs.reject! do |build_attrs|
+ @pipeline.builds.find_by(ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: trigger_request,
+ name: build_attrs[:name])
+ end
+
builds_attrs.map do |build_attrs|
- # don't create the same build twice
- unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag,
- trigger_request: trigger_request, name: build_attrs[:name])
- build_attrs.slice!(:name,
- :commands,
- :tag_list,
- :options,
- :allow_failure,
- :stage,
- :stage_idx,
- :environment)
+ build_attrs.slice!(:name,
+ :commands,
+ :tag_list,
+ :options,
+ :allow_failure,
+ :stage,
+ :stage_idx,
+ :environment)
- build_attrs.merge!(ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: trigger_request,
- user: user,
- project: @pipeline.project)
+ build_attrs.merge!(pipeline: @pipeline,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: trigger_request,
+ user: user,
+ project: @pipeline.project)
- @pipeline.builds.create!(build_attrs)
- end
+ ##
+ # We do not persist new builds here.
+ # Those will be persisted when @pipeline is saved.
+ #
+ @pipeline.builds.new(build_attrs)
end
end
-
- private
-
- def config_processor
- @config_processor ||= @pipeline.config_processor
- end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index a7751b8effc..b1ee6874190 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -8,7 +8,9 @@ module Ci
return pipeline
end
- unless commit
+ if commit
+ pipeline.sha = commit.id
+ else
pipeline.errors.add(:base, 'Commit not found')
return pipeline
end
@@ -18,22 +20,18 @@ module Ci
return pipeline
end
- begin
- Ci::Pipeline.transaction do
- pipeline.sha = commit.id
+ unless pipeline.config_processor
+ pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
+ return pipeline
+ end
- unless pipeline.config_processor
- pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
- raise ActiveRecord::Rollback
- end
+ pipeline.save!
- pipeline.save!
- pipeline.create_builds(current_user)
- end
- rescue
- pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.')
+ unless pipeline.create_builds(current_user)
+ pipeline.errors.add(:base, 'No builds for this pipeline.')
end
+ pipeline.save
pipeline
end
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 4ff268a6f06..f0ed09a629a 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -7,15 +7,19 @@ module Ci
builds =
if current_runner.shared?
- # don't run projects which have not enables shared runners
- builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true })
+ builds.
+ # don't run projects which have not enabled shared runners
+ joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }).
+
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+ order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
else
- # do run projects which are only assigned to this runner
- builds.where(project: current_runner.projects.where(builds_enabled: true))
+ # do run projects which are only assigned to this runner (FIFO)
+ builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC')
end
- builds = builds.order('created_at ASC')
-
build = builds.find do |build|
build.can_be_served?(current_runner)
end
@@ -35,5 +39,12 @@ module Ci
rescue StateMachines::InvalidTransition
nil
end
+
+ private
+
+ def running_builds_for_shared_runners
+ Ci::Build.running.where(runner: Ci::Runner.shared).
+ group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
+ end
end
end
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
index 418f5cf8091..f947e8f452e 100644
--- a/app/services/create_commit_builds_service.rb
+++ b/app/services/create_commit_builds_service.rb
@@ -1,15 +1,11 @@
class CreateCommitBuildsService
def execute(project, user, params)
- return false unless project.builds_enabled?
+ return unless project.builds_enabled?
before_sha = params[:checkout_sha] || params[:before]
sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref]
- unless origin_ref && sha.present?
- return false
- end
-
ref = Gitlab::Git.ref_name(origin_ref)
tag = Gitlab::Git.tag_ref?(origin_ref)
@@ -18,23 +14,50 @@ class CreateCommitBuildsService
return false
end
- pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
+ @pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
- # Skip creating pipeline when no gitlab-ci.yml is found
- unless pipeline.ci_yaml_file
+ ##
+ # Skip creating pipeline if no gitlab-ci.yml is found
+ #
+ unless @pipeline.ci_yaml_file
return false
end
- # Create a new pipeline
- pipeline.save!
-
+ ##
# Skip creating builds for commits that have [ci skip]
- unless pipeline.skip_ci?
- # Create builds for commit
- pipeline.create_builds(user)
+ # but save pipeline object
+ #
+ if @pipeline.skip_ci?
+ return save_pipeline!
+ end
+
+ ##
+ # Skip creating builds when CI config is invalid
+ # but save pipeline object
+ #
+ unless @pipeline.config_processor
+ return save_pipeline!
end
- pipeline.touch
- pipeline
+ ##
+ # Skip creating pipeline object if there are no builds for it.
+ #
+ unless @pipeline.create_builds(user)
+ @pipeline.errors.add(:base, 'No builds created')
+ return false
+ end
+
+ save_pipeline!
+ end
+
+ private
+
+ ##
+ # Create a new pipeline and touch object to calculate status
+ #
+ def save_pipeline!
+ @pipeline.save!
+ @pipeline.touch
+ @pipeline
end
end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index 8f5c3393dfc..d7a0c25a044 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -3,7 +3,7 @@ class GitHooksService
def execute(user, repo_path, oldrev, newrev, ref)
@repo_path = repo_path
- @user = Gitlab::ShellEnv.gl_id(user)
+ @user = Gitlab::GlId.gl_id(user)
@oldrev = oldrev
@newrev = newrev
@ref = ref
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f804ac171c4..c125b8aff29 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -222,7 +222,7 @@ class NotificationService
end
def accept_group_invite(group_member)
- mailer.member_invite_accepted_email(group_member.id).deliver_later
+ mailer.member_invite_accepted_email(group_member.real_source_type, group_member.id).deliver_later
end
def decline_group_invite(group_member)
@@ -266,6 +266,14 @@ class NotificationService
end
end
+ def project_exported(project, current_user)
+ mailer.project_was_exported_email(current_user, project).deliver_later
+ end
+
+ def project_not_exported(project, current_user, errors)
+ mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
+ end
+
protected
# Get project users with WATCH notification level
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index eb73948006e..23b6668e0d1 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -11,5 +11,9 @@ module Projects
def merge_requests
@project.merge_requests.opened.select([:iid, :title])
end
+
+ def labels
+ @project.labels.select([:title, :color])
+ end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 61cac5419ad..55956be2844 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -80,16 +80,18 @@ module Projects
def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
- @project.create_wiki if @project.wiki_enabled?
+ unless @project.gitlab_project_import?
+ @project.create_wiki if @project.wiki_enabled?
- @project.build_missing_services
+ @project.build_missing_services
- @project.create_labels
+ @project.create_labels
+ end
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
- unless @project.group
+ unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user]
end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
new file mode 100644
index 00000000000..d6752377ce5
--- /dev/null
+++ b/app/services/projects/import_export/export_service.rb
@@ -0,0 +1,57 @@
+module Projects
+ module ImportExport
+ class ExportService < BaseService
+
+ def execute(_options = {})
+ @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work'))
+ save_all
+ end
+
+ private
+
+ def save_all
+ if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ Gitlab::ImportExport::Saver.save(shared: @shared)
+ notify_success
+ else
+ cleanup_and_notify
+ end
+ end
+
+ def version_saver
+ Gitlab::ImportExport::VersionSaver.new(shared: @shared)
+ end
+
+ def project_tree_saver
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
+ end
+
+ def uploads_saver
+ Gitlab::ImportExport::UploadsSaver.new(project: project, shared: @shared)
+ end
+
+ def repo_saver
+ Gitlab::ImportExport::RepoSaver.new(project: project, shared: @shared)
+ end
+
+ def wiki_repo_saver
+ Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
+ end
+
+ def cleanup_and_notify
+ FileUtils.rm_rf(@shared.export_path)
+
+ notify_error
+ raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
+ end
+
+ def notify_success
+ notification_service.project_exported(@project, @current_user)
+ end
+
+ def notify_error
+ notification_service.project_not_exported(@project, @current_user, @shared.errors.join(', '))
+ end
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index c4838d31f2f..9159ec08959 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -9,26 +9,31 @@ module Projects
'fogbugz',
'gitlab',
'github',
- 'google_code'
+ 'google_code',
+ 'gitlab_project'
]
def execute
- if unknown_url?
- # In this case, we only want to import issues, not a repository.
- create_repository
- else
- import_repository
- end
+ add_repository_to_project unless project.gitlab_project_import?
import_data
success
- rescue Error => e
+ rescue => e
error(e.message)
end
private
+ def add_repository_to_project
+ if unknown_url?
+ # In this case, we only want to import issues, not a repository.
+ create_repository
+ else
+ import_repository
+ end
+ end
+
def create_repository
unless project.create_repository
raise Error, 'The repository could not be created.'
@@ -46,7 +51,7 @@ module Projects
def import_data
return unless has_importer?
- project.repository.before_import
+ project.repository.before_import unless project.gitlab_project_import?
unless importer.execute
raise Error, 'The remote data could not be imported.'
@@ -58,6 +63,8 @@ module Projects
end
def importer
+ return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import?
+
class_name = "Gitlab::#{project.import_type.camelize}Import::Importer"
class_name.constantize.new(project)
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index e1f9ea64dc4..540bf54b920 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -1,6 +1,6 @@
# TodoService class
#
-# Used for creating todos after certain user actions
+# Used for creating/updating todos after certain user actions
#
# Ex.
# TodoService.new.new_issue(issue, current_user)
@@ -137,6 +137,15 @@ class TodoService
def mark_pending_todos_as_done(target, user)
attributes = attributes_for_target(target)
pending_todos(user, attributes).update_all(state: :done)
+ user.update_todos_count_cache
+ end
+
+ # When user marks some todos as done
+ def mark_todos_as_done(todos, current_user)
+ todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all)
+
+ todos.update_all(state: :done)
+ current_user.update_todos_count_cache
end
# When user marks an issue as todo
@@ -151,6 +160,7 @@ class TodoService
Array(users).map do |user|
next if pending_todos(user, attributes).exists?
Todo.create(attributes.merge(user_id: user.id))
+ user.update_todos_count_cache
end
end
@@ -161,11 +171,16 @@ class TodoService
def update_issuable(issuable, author)
# Skip toggling a task list item in a description
- return if issuable.tasks? && issuable.updated_tasks.any?
+ return if toggling_tasks?(issuable)
create_mention_todos(issuable.project, issuable, author)
end
+ def toggling_tasks?(issuable)
+ issuable.previous_changes.include?('description') &&
+ issuable.tasks? && issuable.updated_tasks.any?
+ end
+
def handle_note(note, author)
# Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet?
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
new file mode 100644
index 00000000000..d78682532ed
--- /dev/null
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -0,0 +1,14 @@
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+ = nav_link(controller: :background_jobs) do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
+ %span
+ Background Jobs
+ = nav_link(controller: :logs) do
+ = link_to admin_logs_path, title: 'Logs' do
+ %span
+ Logs
+ = nav_link(controller: :health_check) do
+ = link_to admin_health_check_path, title: 'Health Check' do
+ %span
+ Health Check
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index de5bc050cf0..654d261aa99 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -1,46 +1,50 @@
+- @no_container = true
- page_title "Background Jobs"
-%h3.page-title Background Jobs
-%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
+= render 'admin/background_jobs/head'
-%hr
+%div{ class: (container_class) }
+ %h3.page-title Background Jobs
+ %p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
-.panel.panel-default
- .panel-heading Sidekiq running processes
- .panel-body
- - if @sidekiq_processes.empty?
- %h4.cred
- %i.fa.fa-exclamation-triangle
- There are no running sidekiq processes. Please restart GitLab
- - else
- .table-holder
- %table.table
- %thead
- %th USER
- %th PID
- %th CPU
- %th MEM
- %th STATE
- %th START
- %th COMMAND
- %tbody
- - @sidekiq_processes.each do |process|
- - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
- - data = process.strip.split(' ')
- %tr
- %td= gitlab_config.user
- - 5.times do
- %td= data.shift
- %td= data.join(' ')
+ %hr
- .clearfix
- %p
- %i.fa.fa-exclamation-circle
- If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
- %p
- %i.fa.fa-exclamation-circle
- If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab.
+ .panel.panel-default
+ .panel-heading Sidekiq running processes
+ .panel-body
+ - if @sidekiq_processes.empty?
+ %h4.cred
+ %i.fa.fa-exclamation-triangle
+ There are no running sidekiq processes. Please restart GitLab
+ - else
+ .table-holder
+ %table.table
+ %thead
+ %th USER
+ %th PID
+ %th CPU
+ %th MEM
+ %th STATE
+ %th START
+ %th COMMAND
+ %tbody
+ - @sidekiq_processes.each do |process|
+ - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
+ - data = process.strip.split(' ')
+ %tr
+ %td= gitlab_config.user
+ - 5.times do
+ %td= data.shift
+ %td= data.join(' ')
+ .clearfix
+ %p
+ %i.fa.fa-exclamation-circle
+ If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'.
+ %p
+ %i.fa.fa-exclamation-circle
+ If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab.
-.panel.panel-default
- %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
+
+ .panel.panel-default
+ %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index d74cf8598e8..efd5b12cfeb 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -1,49 +1,54 @@
-.top-area
- %ul.nav-links
- %li{class: ('active' if @scope.nil?)}
- = link_to admin_builds_path do
- All
- %span.badge.js-totalbuilds-count= @all_builds.count(:id)
-
- %li{class: ('active' if @scope == 'running')}
- = link_to admin_builds_path(scope: :running) do
- Running
- %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id))
-
- %li{class: ('active' if @scope == 'finished')}
- = link_to admin_builds_path(scope: :finished) do
- Finished
- %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id))
-
- .nav-controls
- - if @all_builds.running_or_pending.any?
- = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
-
-.row-content-block.second-block
- #{(@scope || 'all').capitalize} builds
-
-%ul.content-list
- - if @builds.blank?
- %li
- .nothing-here-block No builds to show
- - else
- .table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Project
- %th Commit
- %th Ref
- %th Runner
- %th Name
- %th Tags
- %th Duration
- %th Finished at
- %th
-
- - @builds.each do |build|
- = render "admin/builds/build", build: build
-
- = paginate @builds, theme: 'gitlab'
+- @no_container = true
+= render "admin/dashboard/head"
+
+%div{ class: (container_class) }
+
+ .top-area
+ %ul.nav-links
+ %li{class: ('active' if @scope.nil?)}
+ = link_to admin_builds_path do
+ All
+ %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to admin_builds_path(scope: :running) do
+ Running
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id))
+
+ %li{class: ('active' if @scope == 'finished')}
+ = link_to admin_builds_path(scope: :finished) do
+ Finished
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id))
+
+ .nav-controls
+ - if @all_builds.running_or_pending.any?
+ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+
+ .row-content-block.second-block
+ #{(@scope || 'all').capitalize} builds
+
+ %ul.content-list
+ - if @builds.blank?
+ %li
+ .nothing-here-block No builds to show
+ - else
+ .table-holder
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Project
+ %th Commit
+ %th Ref
+ %th Runner
+ %th Name
+ %th Tags
+ %th Duration
+ %th Finished at
+ %th
+
+ - @builds.each do |build|
+ = render "admin/builds/build", build: build
+
+ = paginate @builds, theme: 'gitlab'
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
new file mode 100644
index 00000000000..7b3f88c24df
--- /dev/null
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -0,0 +1,22 @@
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Overview
+ = nav_link(controller: [:admin, :projects]) do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
+ %span
+ Projects
+ = nav_link(controller: :users) do
+ = link_to admin_users_path, title: 'Users' do
+ %span
+ Users
+ = nav_link(controller: :groups) do
+ = link_to admin_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link path: 'builds#index' do
+ = link_to admin_builds_path, title: 'Builds' do
+ %span
+ Builds
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 6dd2fef395d..4682016a886 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,155 +1,159 @@
-.admin-dashboard.prepend-top-default
- .row
- .col-md-4
- %h4 Statistics
- %hr
- %p
- Forks
- %span.light.pull-right
- = number_with_delimiter(ForkedProjectLink.count)
- %p
- Issues
- %span.light.pull-right
- = number_with_delimiter(Issue.count)
- %p
- Merge Requests
- %span.light.pull-right
- = number_with_delimiter(MergeRequest.count)
- %p
- Notes
- %span.light.pull-right
- = number_with_delimiter(Note.count)
- %p
- Snippets
- %span.light.pull-right
- = number_with_delimiter(Snippet.count)
- %p
- SSH Keys
- %span.light.pull-right
- = number_with_delimiter(Key.count)
- %p
- Milestones
- %span.light.pull-right
- = number_with_delimiter(Milestone.count)
- %p
- Active Users
- %span.light.pull-right
- = number_with_delimiter(User.active.count)
- .col-md-4
- %h4
- Features
- %hr
- %p
- Sign up
- %span.light.pull-right
- = boolean_to_icon signup_enabled?
- %p
- LDAP
- %span.light.pull-right
- = boolean_to_icon Gitlab.config.ldap.enabled
- %p
- Gravatar
- %span.light.pull-right
- = boolean_to_icon gravatar_enabled?
- %p
- OmniAuth
- %span.light.pull-right
- = boolean_to_icon Gitlab.config.omniauth.enabled
- %p
- Reply by email
- %span.light.pull-right
- = boolean_to_icon Gitlab::IncomingEmail.enabled?
- .col-md-4
- %h4
- Components
- - if current_application_settings.version_check_enabled
- .pull-right
- = version_status_badge
+- @no_container = true
+= render "admin/dashboard/head"
- %hr
- %p
- GitLab
- %span.pull-right
- = Gitlab::VERSION
- %p
- GitLab Shell
- %span.pull-right
- = Gitlab::Shell.new.version
- %p
- GitLab API
- %span.pull-right
- = API::API::version
- %p
- Git
- %span.pull-right
- = Gitlab::Git.version
- %p
- Ruby
- %span.pull-right
- #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
-
- %p
- Rails
- %span.pull-right
- #{Rails::VERSION::STRING}
-
- %p
- = Gitlab::Database.adapter_name
- %span.pull-right
- = Gitlab::Database.version
- %hr
- .row
- .col-sm-4
- .light-well
- %h4 Projects
- .data
- = link_to admin_namespaces_projects_path do
- %h1= number_with_delimiter(Project.count)
- %hr
- = link_to('New Project', new_project_path, class: "btn btn-new")
- .col-sm-4
- .light-well
- %h4 Users
- .data
- = link_to admin_users_path do
- %h1= number_with_delimiter(User.count)
- %hr
- = link_to 'New User', new_admin_user_path, class: "btn btn-new"
- .col-sm-4
- .light-well
- %h4 Groups
- .data
- = link_to admin_groups_path do
- %h1= number_with_delimiter(Group.count)
- %hr
- = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
-
- .row.prepend-top-10
- .col-md-4
- %h4 Latest projects
- %hr
- - @projects.each do |project|
+%div{ class: (container_class) }
+ .admin-dashboard.prepend-top-default
+ .row
+ .col-md-4
+ %h4 Statistics
+ %hr
%p
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated'
+ Forks
%span.light.pull-right
- #{time_ago_with_tooltip(project.created_at)}
-
- .col-md-4
- %h4 Latest users
- %hr
- - @users.each do |user|
+ = number_with_delimiter(ForkedProjectLink.count)
%p
- = link_to [:admin, user], class: 'str-truncated' do
- = user.name
+ Issues
%span.light.pull-right
- #{time_ago_with_tooltip(user.created_at)}
-
- .col-md-4
- %h4 Latest groups
- %hr
- - @groups.each do |group|
+ = number_with_delimiter(Issue.count)
+ %p
+ Merge Requests
+ %span.light.pull-right
+ = number_with_delimiter(MergeRequest.count)
+ %p
+ Notes
+ %span.light.pull-right
+ = number_with_delimiter(Note.count)
+ %p
+ Snippets
+ %span.light.pull-right
+ = number_with_delimiter(Snippet.count)
+ %p
+ SSH Keys
+ %span.light.pull-right
+ = number_with_delimiter(Key.count)
+ %p
+ Milestones
+ %span.light.pull-right
+ = number_with_delimiter(Milestone.count)
+ %p
+ Active Users
+ %span.light.pull-right
+ = number_with_delimiter(User.active.count)
+ .col-md-4
+ %h4
+ Features
+ %hr
+ %p
+ Sign up
+ %span.light.pull-right
+ = boolean_to_icon signup_enabled?
%p
- = link_to [:admin, group], class: 'str-truncated' do
- = group.name
+ LDAP
%span.light.pull-right
- #{time_ago_with_tooltip(group.created_at)}
+ = boolean_to_icon Gitlab.config.ldap.enabled
+ %p
+ Gravatar
+ %span.light.pull-right
+ = boolean_to_icon gravatar_enabled?
+ %p
+ OmniAuth
+ %span.light.pull-right
+ = boolean_to_icon Gitlab.config.omniauth.enabled
+ %p
+ Reply by email
+ %span.light.pull-right
+ = boolean_to_icon Gitlab::IncomingEmail.enabled?
+ .col-md-4
+ %h4
+ Components
+ - if current_application_settings.version_check_enabled
+ .pull-right
+ = version_status_badge
+
+ %hr
+ %p
+ GitLab
+ %span.pull-right
+ = Gitlab::VERSION
+ %p
+ GitLab Shell
+ %span.pull-right
+ = Gitlab::Shell.new.version
+ %p
+ GitLab API
+ %span.pull-right
+ = API::API::version
+ %p
+ Git
+ %span.pull-right
+ = Gitlab::Git.version
+ %p
+ Ruby
+ %span.pull-right
+ #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
+
+ %p
+ Rails
+ %span.pull-right
+ #{Rails::VERSION::STRING}
+
+ %p
+ = Gitlab::Database.adapter_name
+ %span.pull-right
+ = Gitlab::Database.version
+ %hr
+ .row
+ .col-sm-4
+ .light-well
+ %h4 Projects
+ .data
+ = link_to admin_namespaces_projects_path do
+ %h1= number_with_delimiter(Project.count)
+ %hr
+ = link_to('New Project', new_project_path, class: "btn btn-new")
+ .col-sm-4
+ .light-well
+ %h4 Users
+ .data
+ = link_to admin_users_path do
+ %h1= number_with_delimiter(User.count)
+ %hr
+ = link_to 'New User', new_admin_user_path, class: "btn btn-new"
+ .col-sm-4
+ .light-well
+ %h4 Groups
+ .data
+ = link_to admin_groups_path do
+ %h1= number_with_delimiter(Group.count)
+ %hr
+ = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
+
+ .row.prepend-top-10
+ .col-md-4
+ %h4 Latest projects
+ %hr
+ - @projects.each do |project|
+ %p
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated'
+ %span.light.pull-right
+ #{time_ago_with_tooltip(project.created_at)}
+
+ .col-md-4
+ %h4 Latest users
+ %hr
+ - @users.each do |user|
+ %p
+ = link_to [:admin, user], class: 'str-truncated' do
+ = user.name
+ %span.light.pull-right
+ #{time_ago_with_tooltip(user.created_at)}
+
+ .col-md-4
+ %h4 Latest groups
+ %hr
+ - @groups.each do |group|
+ %p
+ = link_to [:admin, group], class: 'str-truncated' do
+ = group.name
+ %span.light.pull-right
+ #{time_ago_with_tooltip(group.created_at)}
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 775072a7441..4f1996ef7ab 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,41 +1,45 @@
+- @no_container = true
- page_title "Groups"
-%h3.page-title
- Groups (#{number_with_delimiter(@groups.total_count)})
+= render "admin/dashboard/head"
-%p.light
- Group allows you to keep projects organized.
- Use groups for uniting related projects.
+%div{ class: (container_class) }
+ %h3.page-title
+ Groups (#{number_with_delimiter(@groups.total_count)})
-.top-area
- .nav-search
- = form_tag admin_groups_path, method: :get, class: 'form-inline' do
- = hidden_field_tag :sort, @sort
- = text_field_tag :name, params[:name], class: "form-control"
- = button_tag "Search", class: "btn submit btn-primary"
+ %p.light
+ Group allows you to keep projects organized.
+ Use groups for uniting related projects.
- .nav-controls
- .dropdown.inline
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_groups_path(sort: sort_value_recently_created) do
+ .top-area
+ .nav-search
+ = form_tag admin_groups_path, method: :get, class: 'form-inline' do
+ = hidden_field_tag :sort, @sort
+ = text_field_tag :name, params[:name], class: "form-control"
+ = button_tag "Search", class: "btn submit btn-primary"
+
+ .nav-controls
+ .dropdown.inline
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
= sort_title_recently_created
- = link_to admin_groups_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to admin_groups_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to admin_groups_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
- = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to admin_groups_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to admin_groups_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+ = link_to admin_groups_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
+ = link_to admin_groups_path(sort: sort_value_oldest_updated) do
+ = sort_title_oldest_updated
+ = link_to 'New Group', new_admin_group_path, class: "btn btn-new"
-%ul.content-list
- - @groups.each do |group|
- = render 'group', group: group
+ %ul.content-list
+ - @groups.each do |group|
+ = render 'group', group: group
-= paginate @groups, theme: "gitlab"
+ = paginate @groups, theme: "gitlab"
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index c2313986a7f..7b8407f9152 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -1,49 +1,52 @@
+- @no_container = true
- page_title "Health Check"
+= render 'admin/background_jobs/head'
-%h3.page-title
- Health Check
-.bs-callout.clearfix
- .pull-left
- %p
- Access token is
- %code#health-check-token= current_application_settings.health_check_access_token
- = button_to reset_health_check_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: 'Are you sure you want to reset the health check token?' } do
- = icon('refresh')
- Reset health check access token
-%p.light
- Health information can be retrieved as plain text, JSON, or XML using:
- %ul
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml)
+%div{ class: (container_class) }
+ %h3.page-title
+ Health Check
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ Access token is
+ %code#health-check-token= current_application_settings.health_check_access_token
+ = button_to reset_health_check_token_admin_application_settings_path,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: 'Are you sure you want to reset the health check token?' } do
+ = icon('refresh')
+ Reset health check access token
+ %p.light
+ Health information can be retrieved as plain text, JSON, or XML using:
+ %ul
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml)
-%p.light
- You can also ask for the status of specific services:
- %ul
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database)
- %li
- %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations)
+ %p.light
+ You can also ask for the status of specific services:
+ %ul
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database)
+ %li
+ %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations)
-%hr
-.panel.panel-default
- .panel-heading
- Current Status:
- - if @errors.blank?
- = icon('circle', class: 'cgreen')
- Healthy
- - else
- = icon('warning', class: 'cred')
- Unhealthy
- .panel-body
- - if @errors.blank?
- No Health Problems Detected
- - else
- = @errors
+ %hr
+ .panel.panel-default
+ .panel-heading
+ Current Status:
+ - if @errors.blank?
+ = icon('circle', class: 'cgreen')
+ Healthy
+ - else
+ = icon('warning', class: 'cred')
+ Unhealthy
+ .panel-body
+ - if @errors.blank?
+ No Health Problems Detected
+ - else
+ = @errors
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 698feb571ac..5ddc3b9ea85 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,28 +1,32 @@
+- @no_container = true
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger]
-%ul.nav-links.log-tabs
- - loggers.each do |klass|
- %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
- = link_to klass::file_name, "##{klass::file_name_noext}",
- 'data-toggle' => 'tab'
-.row-content-block
- To prevent performance issues admin logs output the last 2000 lines
-.tab-content
- - loggers.each do |klass|
- .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
- id: klass::file_name_noext }
- .file-holder#README
- .file-title
- %i.fa.fa-file
- = klass::file_name
- .pull-right
- = link_to '#', class: 'log-bottom' do
- %i.fa.fa-arrow-down
- Scroll down
- .file-content.logs
- %ol
- - klass.read_latest.each do |line|
- %li
- %p= line
+= render 'admin/background_jobs/head'
+
+%div{ class: (container_class) }
+ %ul.nav-links.log-tabs
+ - loggers.each do |klass|
+ %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
+ = link_to klass::file_name, "##{klass::file_name_noext}",
+ 'data-toggle' => 'tab'
+ .row-content-block
+ To prevent performance issues admin logs output the last 2000 lines
+ .tab-content
+ - loggers.each do |klass|
+ .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
+ id: klass::file_name_noext }
+ .file-holder#README
+ .file-title
+ %i.fa.fa-file
+ = klass::file_name
+ .pull-right
+ = link_to '#', class: 'log-bottom' do
+ %i.fa.fa-arrow-down
+ Scroll down
+ .file-content.logs
+ %ol
+ - klass.read_latest.each do |line|
+ %li
+ %p= line
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index aa07afa0d62..4822cb693c2 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,94 +1,97 @@
+- @no_container = true
- page_title "Projects"
= render 'shared/show_aside'
+= render "admin/dashboard/head"
-.row.prepend-top-default
- %aside.col-md-3
- .panel.admin-filter
- = form_tag admin_namespaces_projects_path, method: :get, class: '' do
- .form-group
- = label_tag :name, 'Name:'
- = text_field_tag :name, params[:name], class: "form-control"
+%div{ class: (container_class) }
+ .row.prepend-top-default
+ %aside.col-md-3
+ .panel.admin-filter
+ = form_tag admin_namespaces_projects_path, method: :get, class: '' do
+ .form-group
+ = label_tag :name, 'Name:'
+ = text_field_tag :name, params[:name], class: "form-control"
- .form-group
- = label_tag :namespace_id, "Namespace"
- = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large'
+ .form-group
+ = label_tag :namespace_id, "Namespace"
+ = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large'
- .form-group
- %strong Activity
- .checkbox
- = label_tag :with_push do
- = check_box_tag :with_push, 1, params[:with_push]
- %span Projects with push events
- .checkbox
- = label_tag :abandoned do
- = check_box_tag :abandoned, 1, params[:abandoned]
- %span No activity over 6 month
- .checkbox
- = label_tag :with_archived do
- = check_box_tag :with_archived, 1, params[:with_archived]
- %span Show archived projects
+ .form-group
+ %strong Activity
+ .checkbox
+ = label_tag :with_push do
+ = check_box_tag :with_push, 1, params[:with_push]
+ %span Projects with push events
+ .checkbox
+ = label_tag :abandoned do
+ = check_box_tag :abandoned, 1, params[:abandoned]
+ %span No activity over 6 month
+ .checkbox
+ = label_tag :with_archived do
+ = check_box_tag :with_archived, 1, params[:with_archived]
+ %span Show archived projects
- %fieldset
- %strong Visibility level:
- .visibility-levels
- - Project.visibility_levels.each do |label, level|
- .checkbox
- %label
- = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s)
- %span.descr
- = visibility_level_icon(level)
- = label
- %fieldset
- %strong Problems
- .checkbox
- = label_tag :last_repository_check_failed do
- = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed]
- %span Last repository check failed
+ %fieldset
+ %strong Visibility level:
+ .visibility-levels
+ - Project.visibility_levels.each do |label, level|
+ .checkbox
+ %label
+ = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s)
+ %span.descr
+ = visibility_level_icon(level)
+ = label
+ %fieldset
+ %strong Problems
+ .checkbox
+ = label_tag :last_repository_check_failed do
+ = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed]
+ %span Last repository check failed
- = hidden_field_tag :sort, params[:sort]
- = button_tag "Search", class: "btn submit btn-primary"
- = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel"
+ = hidden_field_tag :sort, params[:sort]
+ = button_tag "Search", class: "btn submit btn-primary"
+ = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel"
- %section.col-md-9
- .panel.panel-default
- .panel-heading
- Projects (#{@projects.total_count})
- .controls
- .dropdown.inline
- %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do
+ %section.col-md-9
+ .panel.panel-default
+ .panel-heading
+ Projects (#{@projects.total_count})
+ .controls
+ .dropdown.inline
+ %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
= sort_title_recently_created
- = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
- = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do
- = sort_title_largest_repo
- = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success"
- %ul.well-list
- - @projects.each do |project|
- %li
- .list-item-name
- %span{ class: visibility_level_color(project.visibility_level) }
- = visibility_level_icon(project.visibility_level)
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
- .pull-right
- - if project.archived
- %span.label.label-warning archived
- %span.label.label-gray
- = repository_size(project)
- = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
- = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove"
- - if @projects.blank?
- .nothing-here-block 0 projects matches
- = paginate @projects, theme: "gitlab"
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+ = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
+ = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do
+ = sort_title_oldest_updated
+ = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do
+ = sort_title_largest_repo
+ = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success"
+ %ul.well-list
+ - @projects.each do |project|
+ %li
+ .list-item-name
+ %span{ class: visibility_level_color(project.visibility_level) }
+ = visibility_level_icon(project.visibility_level)
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ .pull-right
+ - if project.archived
+ %span.label.label-warning archived
+ %span.label.label-gray
+ = repository_size(project)
+ = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
+ = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove"
+ - if @projects.blank?
+ .nothing-here-block 0 projects matches
+ = paginate @projects, theme: "gitlab"
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index d6743081c8e..d0a696da64b 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,107 +1,110 @@
+- @no_container = true
- page_title "Users"
= render 'shared/show_aside'
+= render "admin/dashboard/head"
-.admin-filter
- %ul.nav-links
- %li{class: "#{'active' unless params[:filter]}"}
- = link_to admin_users_path do
- Active
- %small.badge= number_with_delimiter(User.active.count)
- %li{class: "#{'active' if params[:filter] == "admins"}"}
- = link_to admin_users_path(filter: "admins") do
- Admins
- %small.badge= number_with_delimiter(User.admins.count)
- %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
- = link_to admin_users_path(filter: 'two_factor_enabled') do
- 2FA Enabled
- %small.badge= number_with_delimiter(User.with_two_factor.count)
- %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
- = link_to admin_users_path(filter: 'two_factor_disabled') do
- 2FA Disabled
- %small.badge= number_with_delimiter(User.without_two_factor.count)
- %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
- = link_to admin_users_path(filter: 'external') do
- External
- %small.badge= number_with_delimiter(User.external.count)
- %li{class: "#{'active' if params[:filter] == "blocked"}"}
- = link_to admin_users_path(filter: "blocked") do
- Blocked
- %small.badge= number_with_delimiter(User.blocked.count)
- %li{class: "#{'active' if params[:filter] == "wop"}"}
- = link_to admin_users_path(filter: "wop") do
- Without projects
- %small.badge= number_with_delimiter(User.without_projects.count)
+%div{ class: (container_class) }
+ .admin-filter
+ %ul.nav-links
+ %li{class: "#{'active' unless params[:filter]}"}
+ = link_to admin_users_path do
+ Active
+ %small.badge= number_with_delimiter(User.active.count)
+ %li{class: "#{'active' if params[:filter] == "admins"}"}
+ = link_to admin_users_path(filter: "admins") do
+ Admins
+ %small.badge= number_with_delimiter(User.admins.count)
+ %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_enabled') do
+ 2FA Enabled
+ %small.badge= number_with_delimiter(User.with_two_factor.count)
+ %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_disabled') do
+ 2FA Disabled
+ %small.badge= number_with_delimiter(User.without_two_factor.count)
+ %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"}
+ = link_to admin_users_path(filter: 'external') do
+ External
+ %small.badge= number_with_delimiter(User.external.count)
+ %li{class: "#{'active' if params[:filter] == "blocked"}"}
+ = link_to admin_users_path(filter: "blocked") do
+ Blocked
+ %small.badge= number_with_delimiter(User.blocked.count)
+ %li{class: "#{'active' if params[:filter] == "wop"}"}
+ = link_to admin_users_path(filter: "wop") do
+ Without projects
+ %small.badge= number_with_delimiter(User.without_projects.count)
- .row-content-block.second-block
- .pull-right
- .dropdown.inline
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_name
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
+ .row-content-block.second-block
+ .pull-right
+ .dropdown.inline
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
= sort_title_name
- = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
- = sort_title_recently_signin
- = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
- = sort_title_oldest_signin
- = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
- = sort_title_recently_created
- = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
- = sort_title_oldest_created
- = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
- = sort_title_recently_updated
- = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
- = sort_title_oldest_updated
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
+ = sort_title_name
+ = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
+ = sort_title_recently_signin
+ = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
+ = sort_title_oldest_signin
+ = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
+ = sort_title_recently_created
+ = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
+ = sort_title_oldest_created
+ = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
+ = sort_title_recently_updated
+ = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
+ = sort_title_oldest_updated
- = link_to 'New User', new_admin_user_path, class: "btn btn-new"
- = form_tag admin_users_path, method: :get, class: 'form-inline' do
- .form-group
- = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
- = hidden_field_tag "filter", params[:filter]
- = button_tag class: 'btn btn-primary' do
- %i.fa.fa-search
+ = link_to 'New User', new_admin_user_path, class: "btn btn-new"
+ = form_tag admin_users_path, method: :get, class: 'form-inline' do
+ .form-group
+ = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
+ = hidden_field_tag "filter", params[:filter]
+ = button_tag class: 'btn btn-primary' do
+ %i.fa.fa-search
-.panel.panel-default
- %ul.well-list
- - @users.each do |user|
- %li
- .list-item-name
- - if user.blocked?
- = icon("lock", class: "cred")
- - else
- = icon("user", class: "cgreen")
- = link_to user.name, [:admin, user]
- - if user.admin?
- %strong.cred (Admin)
- - if user.external?
- %strong.cred (External)
- - if user == current_user
- %span.cred It's you!
- .pull-right
- %span.light
- %i.fa.fa-envelope
- = mail_to user.email, user.email, class: 'light'
- &nbsp;
+ .panel.panel-default
+ %ul.well-list
+ - @users.each do |user|
+ %li
+ .list-item-name
+ - if user.blocked?
+ = icon("lock", class: "cred")
+ - else
+ = icon("user", class: "cgreen")
+ = link_to user.name, [:admin, user]
+ - if user.admin?
+ %strong.cred (Admin)
+ - if user.external?
+ %strong.cred (External)
+ - if user == current_user
+ %span.cred It's you!
.pull-right
- = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
- - unless user == current_user
- - if user.ldap_blocked?
- = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
- %i.fa.fa-lock
- Unblock
- - elsif user.blocked?
- = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- - else
- = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
- - if user.access_locked?
- = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- - if user.can_be_removed?
- = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
-= paginate @users, theme: "gitlab"
+ %span.light
+ %i.fa.fa-envelope
+ = mail_to user.email, user.email, class: 'light'
+ &nbsp;
+ .pull-right
+ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
+ - unless user == current_user
+ - if user.ldap_blocked?
+ = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
+ %i.fa.fa-lock
+ Unblock
+ - elsif user.blocked?
+ = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
+ - else
+ = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
+ - if user.access_locked?
+ = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
+ - if user.can_be_removed?
+ = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
+ = paginate @users, theme: "gitlab"
diff --git a/app/views/devise/mailer/password_change.html.haml b/app/views/devise/mailer/password_change.html.haml
new file mode 100644
index 00000000000..3349ee84807
--- /dev/null
+++ b/app/views/devise/mailer/password_change.html.haml
@@ -0,0 +1,10 @@
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ The password for your GitLab account on
+ #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}
+ has successfully been changed.
+ %p
+ If you did not initiate this change, please contact your administrator
+ immediately.
diff --git a/app/views/devise/mailer/password_change.text.erb b/app/views/devise/mailer/password_change.text.erb
new file mode 100644
index 00000000000..95923d9f8de
--- /dev/null
+++ b/app/views/devise/mailer/password_change.text.erb
@@ -0,0 +1,7 @@
+Hello, <%= @resource.name %>!
+
+The password for your GitLab account on <%= Gitlab.config.gitlab.url %>
+has successfully been changed.
+
+If you did not initiate this change, please contact your administrator
+immediately.
diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb
deleted file mode 100644
index 23b31da92d8..00000000000
--- a/app/views/devise/mailer/reset_password_instructions.html.erb
+++ /dev/null
@@ -1,8 +0,0 @@
-<p>Hello <%= @resource.email %>!</p>
-
-<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
-
-<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p>
-
-<p>If you didn't request this, please ignore this email.</p>
-<p>Your password won't change until you access the link above and create a new one.</p>
diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml
new file mode 100644
index 00000000000..e91c9522520
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.html.haml
@@ -0,0 +1,12 @@
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ Someone, hopefully you, has requested to reset the password for your
+ GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}.
+ %p
+ If you did not perform this request, you can safely ignore this email.
+ %p
+ Otherwise, click the link below to complete the process.
+ #cta
+ = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token))
diff --git a/app/views/devise/mailer/reset_password_instructions.text.erb b/app/views/devise/mailer/reset_password_instructions.text.erb
new file mode 100644
index 00000000000..116313ee11c
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.text.erb
@@ -0,0 +1,10 @@
+Hello, <%= @resource.name %>!
+
+Someone, hopefully you, has requested to reset the password for your GitLab
+account on <%= Gitlab.config.gitlab.url %>
+
+If you did not perform this request, you can safely ignore this email.
+
+Otherwise, click the link below to complete the process:
+
+<%= edit_password_url(@resource, reset_password_token: @token) %>
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
index 52b327e20c5..9990d1ccac6 100644
--- a/app/views/devise/mailer/unlock_instructions.html.haml
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -1,10 +1,9 @@
-%p
-Hello #{@resource.name}!
-
-%p
- Your GitLab account has been locked due to an excessive amount of unsuccessful
- sign in attempts. Your account will automatically unlock in
- = time_ago_in_words(Devise.unlock_in.from_now)
- or you may click the link below to unlock now.
-
-%p= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token)
+.center
+ #content
+ %h2 Hello, #{@resource.name}!
+ %p
+ Your GitLab account has been locked due to an excessive amount of unsuccessful
+ sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)}
+ or you may click the link below to unlock now.
+ #cta
+ = link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb
new file mode 100644
index 00000000000..3aea3e20145
--- /dev/null
+++ b/app/views/devise/mailer/unlock_instructions.text.erb
@@ -0,0 +1,7 @@
+Hello, <%= @resource.name %>!
+
+Your GitLab account has been locked due to an excessive amount of unsuccessful
+sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %>
+or you may click the link below to unlock now.
+
+<%= unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index b0b3a51ce58..da71de4cd1e 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,2 @@
:plain
- $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member))}');
+ $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
new file mode 100644
index 00000000000..44e2653ca4a
--- /dev/null
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -0,0 +1,25 @@
+- page_title "GitLab Import"
+- header_title "Projects", root_path
+%h3.page-title
+ = icon('gitlab')
+ Import an exported GitLab project
+%hr
+
+= form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do
+ %p
+ Project will be imported as
+ %strong
+ #{@namespace_name}/#{@path}
+
+ %p
+ To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
+ .form-group
+ = hidden_field_tag :namespace_id, @namespace_id
+ = hidden_field_tag :path, @path
+ = label_tag :file, class: 'control-label' do
+ %span GitLab project export
+ .col-sm-10
+ = file_field_tag :file, class: ''
+
+ .form-actions
+ = submit_tag 'Import project', class: 'btn btn-create'
diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml
index e4fab897377..8c140a5943e 100644
--- a/app/views/layouts/_collapse_button.html.haml
+++ b/app/views/layouts/_collapse_button.html.haml
@@ -1 +1,3 @@
-= link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
+= link_to '#', class: 'nav-header-btn text-center toggle-nav-collapse', title: "Open/Close" do
+ %span.sr-only Toggle navigation
+ = icon('bars')
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index f89e8582792..199ab3c38c3 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,6 +1,6 @@
-.page-with-sidebar.page-sidebar-collapsed{ class: "#{page_gutter_class}" }
+.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
-
+ = render partial: 'layouts/collapse_button'
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
@@ -8,13 +8,14 @@
- else
= render 'layouts/nav/explore'
- .collapse-nav
- = render partial: 'layouts/collapse_button'
- if current_user
= link_to current_user, class: 'sidebar-user', title: "Profile", data: {user: current_user.username} do
= image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
.username
= current_user.username
+ = link_to '#', class: "nav-header-btn text-center pin-nav-btn #{'is-active' if pinned_nav?} js-nav-pin", title: 'Pin/Unpin navigation' do
+ %span.sr-only Toggle navigation pinning
+ = icon('thumb-tack')
- if defined?(nav) && nav
.layout-nav
.container-fluid
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index b49207fc315..245b9c3b4d4 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -36,6 +36,31 @@
- else
= hidden_field_tag :search_code, true
+ :javascript
+ gl.projectOptions = gl.projectOptions || {};
+ gl.projectOptions["#{j(@project.path)}"] = {
+ issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}",
+ mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}",
+ name: "#{j(@project.name)}"
+ };
+
+ - if @group and @group.path
+ :javascript
+ gl.groupOptions = gl.groupOptions || {};
+ gl.groupOptions["#{j(@group.path)}"] = {
+ name: "#{j(@group.name)}",
+ issuesPath: "#{issues_group_path(j(@group.path))}",
+ mrPath: "#{merge_requests_group_path(j(@group.path))}"
+ };
+
+
+ :javascript
+ gl.dashboardOptions = {
+ issuesPath: "#{issues_dashboard_url}",
+ mrPath: "#{merge_requests_dashboard_url}"
+ };
+
+
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 6591c52bdbd..87064cc9b3f 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,5 +1,5 @@
- page_title "Admin Area"
- header_title "Admin Area", admin_root_path
-- sidebar "admin"
+- nav "admin"
= render template: "layouts/application"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 2b86b289bbe..33cedaaf2ee 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: "en"}
= render "layouts/head"
- %body{class: "#{user_application_theme}", 'data-page' => body_data_page}
+ %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
= Gon::Base.render_data
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index ef31520f5cb..40a2c81eebd 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab.header-collapsed{ class: nav_header_class }
+%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
.header-content
%button.side-nav-toggle{type: 'button'}
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index f292730fe45..54aa34bee0b 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,107 +1,64 @@
-%ul.nav.nav-sidebar
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- = icon('dashboard fw')
+%ul.nav-links.scrolling-tabs
+ .fade-left
+ = nav_link(controller: %w(dashboard admin projects users groups builds), html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- = icon('cube fw')
+ = nav_link(controller: %w(background_jobs logs health_check)) do
+ = link_to admin_background_jobs_path, title: 'Monitoring' do
%span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- = icon('user fw')
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- = icon('group fw')
- %span
- Groups
+ Monitoring
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
- = icon('key fw')
%span
Deploy Keys
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
- = icon('cog fw')
%span
Runners
- %span.count= number_with_delimiter(Ci::Runner.count(:all))
- = nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
- = icon('link fw')
- %span
- Builds
- %span.count= number_with_delimiter(Ci::Build.count(:all))
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- = icon('file-text fw')
- %span
- Logs
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: 'Health Check' do
- = icon('medkit fw')
- %span
- Health Check
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do
- = icon('bullhorn fw')
%span
Messages
= nav_link(controller: :hooks) do
= link_to admin_hooks_path, title: 'Hooks' do
- = icon('external-link fw')
%span
Hooks
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- = icon('cog fw')
- %span
- Background Jobs
+
= nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do
- = icon('image')
%span
Appearance
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
- = icon('cloud fw')
%span
Applications
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do
- = icon('copy fw')
%span
Service Templates
= nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do
- = icon('tags fw')
%span
Labels
= nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
- = icon('exclamation-circle fw')
%span
Abuse Reports
- %span.count= number_with_delimiter(AbuseReport.count(:all))
+ %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- if askimet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
- = icon('exclamation-triangle fw')
%span
Spam Logs
- %span.count= number_with_delimiter(SpamLog.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to admin_application_settings_path, title: 'Settings' do
- = icon('cogs fw')
%span
Settings
+ .fade-right
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index d4b1f477f3f..bb6f14a6225 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -13,6 +13,10 @@
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
+ = nav_link(controller: :personal_access_tokens) do
+ = link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do
+ %span
+ Personal Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index a851cae4b56..39ea4920ccc 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -5,18 +5,19 @@
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
+ - is_project_member = @project.users.exists?(current_user.id)
- access = @project.team.max_member_access(current_user.id)
- can_edit = can?(current_user, :admin_project, @project)
= render 'layouts/nav/project_settings', access: access, can_edit: can_edit
- - if can_edit || access
+ - if can_edit || is_project_member
%li.divider
- if can_edit
%li
= link_to edit_project_path(@project) do
Edit Project
- - if access
+ - if is_project_member
%li
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
new file mode 100644
index 00000000000..b28fea35ad5
--- /dev/null
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -0,0 +1,8 @@
+%p
+ Project #{@project.name} was exported successfully.
+%p
+ The project export can be downloaded from:
+ = link_to download_export_namespace_project_url(@project.namespace, @project) do
+ = @project.name_with_namespace + " export"
+%p
+ The download link will expire in 24 hours.
diff --git a/app/views/notify/project_was_exported_email.text.erb b/app/views/notify/project_was_exported_email.text.erb
new file mode 100644
index 00000000000..42c4d176876
--- /dev/null
+++ b/app/views/notify/project_was_exported_email.text.erb
@@ -0,0 +1,6 @@
+Project <%= @project.name %> was exported successfully.
+
+The project export can be downloaded from:
+<%= download_export_namespace_project_url(@project.namespace, @project) %>
+
+The download link will expire in 24 hours.
diff --git a/app/views/notify/project_was_not_exported_email.html.haml b/app/views/notify/project_was_not_exported_email.html.haml
new file mode 100644
index 00000000000..c9e9ade2cf1
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.html.haml
@@ -0,0 +1,9 @@
+%p
+ Project #{@project.name} couldn't be exported.
+%p
+ The errors we encountered were:
+
+ %ul
+ - @errors.each do |error|
+ %li
+ error
diff --git a/app/views/notify/project_was_not_exported_email.text.erb b/app/views/notify/project_was_not_exported_email.text.erb
new file mode 100644
index 00000000000..a07f6edacf7
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.text.erb
@@ -0,0 +1,6 @@
+Project <%= @project.name %> couldn't be exported.
+
+The errors we encountered were:
+
+- @errors.each do |error|
+<%= error %> \ No newline at end of file
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
new file mode 100644
index 00000000000..1b45548bd02
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -0,0 +1,105 @@
+- page_title "Personal Access Tokens"
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can generate a personal access token for each application you use that needs access to the GitLab API.
+ .col-lg-9
+
+ - if flash[:personal_access_token]
+ .created-personal-access-token-container
+ %h5.prepend-top-0
+ Your New Personal Access Token
+ .form-group
+ = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
+ = clipboard_button(clipboard_text: flash[:personal_access_token])
+ %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
+
+ %hr
+
+ %h5.prepend-top-0
+ Add a Personal Access Token
+ %p.profile-settings-content
+ Pick a name for the application, and we'll give you a unique token.
+ = form_for [:profile, @personal_access_token],
+ method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(@personal_access_token)
+
+ .form-group
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
+
+ .form-group
+ = f.label :expires_at, class: 'label-light'
+ = f.text_field :expires_at, class: "datepicker form-control", required: false
+
+ .prepend-top-default
+ = f.submit 'Create Personal Access Token', class: "btn btn-create"
+
+ %hr
+
+ %h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
+
+ - if @active_personal_access_tokens.present?
+ .table-responsive
+ %table.table.active-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %th Expires
+ %th
+ %tbody
+ - @active_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+ %td
+ - if token.expires_at.present?
+ = token.expires_at.to_date.to_s(:medium)
+ - else
+ %span.personal-access-tokens-never-expires-label Never
+ %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
+
+ - else
+ .settings-message.text-center
+ You don't have any active tokens yet.
+
+ %hr
+
+ %h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
+
+ - if @inactive_personal_access_tokens.present?
+ .table-responsive
+ %table.table.inactive-personal-access-tokens
+ %thead
+ %tr
+ %th Name
+ %th Created
+ %tbody
+ - @inactive_personal_access_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+
+ - else
+ .settings-message.text-center
+ There are no inactive tokens.
+
+
+:javascript
+ var date = $('#personal_access_token_expires_at').val();
+
+ var datepicker = $(".datepicker").datepicker({
+ dateFormat: "yy-mm-dd",
+ minDate: 0
+ });
+
+ $("#created-personal-access-token").click(function() {
+ this.select();
+ });
+
+ $("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 7c2b8d01508..e0ca2a3109c 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,15 +1,15 @@
- if event = last_push_event
- if show_last_push_widget?(event)
-
.row-content-block.top-block.clear-block.hidden-xs
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
- %strong= event.ref_name
- branch
- #{time_ago_with_tooltip(event.created_at)}
+ %div{ class: (container_class) }
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ %strong= event.ref_name
+ branch
+ #{time_ago_with_tooltip(event.created_at)}
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
- Create Merge Request
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
+ Create Merge Request
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 4071b59c003..ae89637df60 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -13,12 +13,10 @@
required: true, class: 'form-control new-file-name'
.pull-right
- .license-selector.js-license-selector.hide
- = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
-
- .gitignore-selector.hidden
- = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } )
-
+ .license-selector.js-license-selector-wrap.hidden
+ = dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
+ .gitignore-selector.js-gitignore-selector-wrap.hidden
+ = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index a26f8aeb315..4e2702c2e44 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -48,16 +48,16 @@
- if @build.active?
.autoscroll-container
%button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- #js-build-scroll.scroll-controls
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
- if @build.erased?
.erased.alert.alert-warning
- erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
- else
+ #js-build-scroll.scroll-controls
+ = link_to '#build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index a0ffa065067..b8d8758fd2b 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -60,7 +60,7 @@
%li
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
= icon("download")
- %span #{build.name}
+ %span Download '#{build.name}' artifacts
- if can?(current_user, :update_pipeline, @project)
- if pipeline.retryable?
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 367027182b6..a959b34a539 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,26 +9,30 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
+ = commit_author_avatar(commit, size: 36)
.commit-row-title
%span.item-title
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
+ %span.commit-row-message.visible-xs-inline
+ &middot;
+ = commit.short_id
+ - if commit.status
+ = render_commit_status(commit, cssclass: 'visible-xs-inline')
- if commit.description?
- %a.text-expander.js-toggle-button ...
+ %a.text-expander.hidden-xs.js-toggle-button ...
- .pull-right
+ .commit-actions.hidden-xs
- if commit.status
- = render_commit_status(commit)
- = clipboard_button(clipboard_text: commit.id)
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+ = render_commit_status(commit, cssclass: 'btn btn-transparent')
+ = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent')
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+ = link_to_browse_code(project, commit)
- if commit.description?
- .commit-row-description.js-toggle-content
- %pre
- = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
+ %pre.commit-row-description.js-toggle-content
+ = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
.commit-row-info
- by
- = commit_author_link(commit, avatar: true, size: 24)
- .committed_ago
- #{time_ago_with_tooltip(commit.committed_date)} &nbsp;
- = link_to_browse_code(project, commit)
+ = commit_author_link(commit, avatar: false, size: 24)
+ authored
+ #{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 7283a78a64e..dd12eae8f7e 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -4,18 +4,11 @@
- commits, hidden = limited_commits(@commits)
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
- .row.commits-row
- .col-md-2.hidden-xs.hidden-sm
- %h5.commits-row-date
- %i.fa.fa-calendar
- %span= day.strftime('%d %b, %Y')
- .light
- = pluralize(commits.count, 'commit')
- .col-md-10.col-sm-12
- %ul.content-list
- = render commits, project: project
- %hr.lists-separator
+ %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}"
+ %li.commits-row
+ %ul.list-unstyled.commit-list
+ = render commits, project: project
- if hidden > 0
- .alert.alert-warning
+ %li.alert.alert-warning
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 76ba0bea36d..51ca4eb903e 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -23,21 +23,18 @@
Create Merge Request
.control
- = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do
- = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false }
-
+ = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
+ = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
- if current_user && current_user.private_token
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do
= icon("rss")
-
-
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
%div{id: dom_id(@project)}
- #commits-list.content_list= render "commits", project: @project
- .clear
+ %ol#commits-list.list-unstyled.content_list
+ = render "commits", project: @project
= spinner
:javascript
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 6c11afbe420..f18bc8c41b3 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -11,6 +11,8 @@
= commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs')
- elsif current_controller?(:merge_requests)
= diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs')
+ - elsif current_controller?(:compare)
+ = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs')
.btn-group
= inline_diff_btn
= parallel_diff_btn
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 8449fe1e4e0..27a94fe02dc 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -120,6 +120,42 @@
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save"
%hr
+ .row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Export project
+ %p.append-bottom-0
+ %p
+ Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
+ %p
+ Once the exported file is ready, you will receive a notification email with a download link.
+
+ .col-lg-9
+
+ - if @project.export_project_path
+ = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
+ method: :get, class: "btn btn-default"
+ = link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-default"
+ - else
+ = link_to 'Export project', export_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-default"
+
+ .bs-callout.bs-callout-info
+ %p.append-bottom-0
+ %p
+ The following items will be exported:
+ %ul
+ %li Project and wiki repositories
+ %li Project uploads
+ %li Project configuration including web hooks and services
+ %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
+ %p
+ The following items will NOT be exported:
+ %ul
+ %li Build traces and artifacts
+ %li LFS objects
+ %hr
- if can? current_user, :archive_project, @project
.row.prepend-top-default
.col-lg-3
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index c0d1ce0d120..4d8ee562e6a 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -7,7 +7,7 @@
Forking in progress.
- else
Import in progress.
- - unless @project.forked?
+ - if @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 6e1baa46b05..aa4d69550ec 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -4,9 +4,10 @@
= render "projects/issues/head"
%div{ class: (container_class) }
- .top-area
+ .top-area.adjust
.nav-text
- Labels can be applied to issues and merge requests.
+ Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
+
.nav-controls
- if can?(current_user, :admin_label, @project)
= link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
@@ -19,10 +20,9 @@
.prioritized-labels{ class: ('hide' if hide) }
%h5 Prioritized Labels
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
+ %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- if @prioritized_labels.present?
= render @prioritized_labels
- - else
- %p.empty-message No prioritized labels yet
.other-labels
- if can?(current_user, :admin_label, @project)
%h5{ class: ('hide' if hide) } Other Labels
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index b08524574e4..de39964fca8 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
+ = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index c4df8bd504f..2ec96308fd7 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -17,11 +17,11 @@
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
- %span.dropdown
+ %span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as
%span.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal
@@ -37,7 +37,7 @@
= render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
- .light.prepend-top-default
+ .light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a8f09f855d4..0b05785430b 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -2,4 +2,5 @@
= icon("sort-amount-desc")
Most recent commits displayed first
-= render "projects/commits/commits", project: @merge_request.project
+%ol#commits-list.list-unstyled
+ = render "projects/commits/commits", project: @merge_request.project
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 0dbd159298e..b3bea900d42 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -8,7 +8,7 @@
%p
%strong Step 1.
Fetch and check out the branch for this merge request
- = clipboard_button(clipboard_target: 'pre#merge-info-1')
+ = clipboard_button_with_class({clipboard_target: "pre#merge-info-1"}, css_class: "btn-clipboard")
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
@@ -25,7 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
- = clipboard_button(clipboard_target: 'pre#merge-info-3')
+ = clipboard_button_with_class({clipboard_target: "pre#merge-info-3"}, css_class: "btn-clipboard")
%pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
@@ -38,7 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
- = clipboard_button(clipboard_target: 'pre#merge-info-4')
+ = clipboard_button_with_class({clipboard_target: "pre#merge-info-4"}, css_class: "btn-clipboard")
%pre.dark#merge-info-4
:preserve
git push origin #{h @merge_request.target_branch}
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index f5e2b927da8..cbf1ba04170 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -19,6 +19,7 @@
= f.label :due_date, "Due Date", class: "control-label"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
+ %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
.form-actions
- if @milestone.new_record?
@@ -27,10 +28,3 @@
-else
= f.submit 'Save changes', class: "btn-save btn"
= link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel"
-
-
-:javascript
- $(".datepicker").datepicker({
- dateFormat: "yy-mm-dd",
- onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
- }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index bf9baaea889..e4ab064eda8 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Network", @ref
+- page_specific_javascripts asset_path("network/application.js")
= render "projects/commits/head"
= render "head"
%div{ class: (container_class) }
@@ -14,14 +15,5 @@
= check_box_tag :filter_ref, 1, @options[:filter_ref]
%span Begin with the selected commit
- .network-graph
+ .network-graph{ data: { url: '#{escape_javascript(@url)}', commit_url: '#{escape_javascript(@commit_url)}', ref: '#{escape_javascript(@ref)}', commit_id: '#{escape_javascript(@commit.id)}' } }
= spinner nil, true
-
-:javascript
- network_graph = new Network({
- url: "#{escape_javascript(@url)}",
- commit_url: "#{escape_javascript(@commit_url)}",
- ref: "#{escape_javascript(@ref)}",
- commit_id: '#{@commit.id}'
- })
- new ShortcutsNetwork(network_graph.branch_graph)
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index f9ac16b32f3..3c1c6060504 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -11,26 +11,22 @@
.project-edit-content
= form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f|
- .form-group.project-name-holder
+ .form-group
= f.label :path, class: 'control-label' do
- Project path
+ Project owner
.col-sm-10
- .input-group
- - if current_user.can_select_namespace?
- .input-group-addon
- = root_url
- = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
- .input-group-addon
- \/
- - else
- .input-group-addon
- #{root_url}#{current_user.username}/
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
-
+ = f.select :namespace_id, namespaces_options(:current_user), {}, {class: 'select2 js-select-namespace', tabindex: 1}
+
- if current_user.can_create_group?
.help-block
Want to house several dependent projects under the same namespace?
= link_to "Create a group", new_group_path
+
+ .form-group
+ = f.label :path, class: 'control-label' do
+ Project name
+ .col-sm-10
+ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
- if import_sources_enabled?
.project-import.js-toggle-container
@@ -88,7 +84,12 @@
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
%i.fa.fa-git
- %span Any repo by URL
+ %span Repo by URL
+
+ - if gitlab_project_import_enabled?
+ = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do
+ %i.fa.fa-gitlab
+ %span GitLab export
.js-toggle-content.hide
= render "shared/import_form", f: f
@@ -119,6 +120,33 @@
e.preventDefault();
var import_modal = $(this).next(".modal").show();
});
+
$('.modal-header .close').bind('click', function() {
$(".modal").hide();
});
+
+ $('.import_gitlab_project').bind('click', function() {
+ var _href = $("a.import_gitlab_project").attr("href");
+ $(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
+ });
+
+ $('.import_gitlab_project').attr('disabled',true)
+ $('.import_gitlab_project').attr('title', 'Project path required.');
+
+ $('.import_gitlab_project').click(function( event ) {
+ if($('.import_gitlab_project').attr('disabled')) {
+ event.preventDefault();
+ new Flash("Please enter a path for the project to be imported to.");
+ }
+ });
+
+ $('#project_path').keyup(function(){
+ if($(this).val().length !=0) {
+ $('.import_gitlab_project').attr('disabled', false);
+ $('.import_gitlab_project').attr('title','');
+ $(".flash-container").html("")
+ } else {
+ $('.import_gitlab_project').attr('disabled',true);
+ $('.import_gitlab_project').attr('title', 'Project path required.');
+ }
+ })
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 2fb3a41d541..45f8ef89060 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,2 @@
:plain
- $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}');
+ $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4afa902b4eb..e9ca46a74bf 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -23,10 +23,10 @@
#{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)})
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
- #{'Branch'.pluralize(@repository.branch_names.count)} (#{number_with_delimiter(@repository.branch_names.count)})
+ #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
%li
= link_to namespace_project_tags_path(@project.namespace, @project) do
- #{'Tag'.pluralize(@repository.tag_names.count)} (#{number_with_delimiter(@repository.tag_names.count)})
+ #{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- if default_project_view != 'readme' && @repository.readme
%li
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 2779084fe38..4ca1f58ac5c 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -11,12 +11,23 @@
.nav-controls
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
New tag
+ .dropdown.inline
+ %button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
+ %span.light= @sort.humanize
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ = link_to namespace_project_tags_path(sort: nil) do
+ Name
+ = link_to namespace_project_tags_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
+ = link_to namespace_project_tags_path(sort: sort_value_oldest_updated) do
+ = sort_title_oldest_updated
.tags
- unless @tags.empty?
%ul.content-list
- - @tags.each do |tag|
- = render 'tag', tag: @repository.find_tag(tag)
+ = render partial: 'tag', collection: @tags
= paginate @tags, theme: 'gitlab'
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index 2ddc5d504fa..a3a4dba3fa4 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -1,8 +1,9 @@
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
= tree_icon(type, blob_item.mode, blob_item.name)
- %span.str-truncated
- = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name))
+ - file_name = blob_item.name
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
+ %span.str-truncated= file_name
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index cf65057e704..9577696fc0d 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -1,9 +1,9 @@
%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
%td.tree-item-file-name
= tree_icon(type, tree_item.mode, tree_item.name)
- %span.str-truncated
- - path = flatten_tree(tree_item)
- = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
+ - path = flatten_tree(tree_item)
+ = link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do
+ %span.str-truncated= path
%td.tree_time_ago.cgray
= render 'projects/tree/spinner'
%td.hidden-xs.tree_commit
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 478c04318c6..77676454b57 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,5 +1,7 @@
%span.label-row
- if can?(current_user, :admin_label, @project)
+ .draggable-handler
+ = icon('bars')
.js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label),
dom_id: dom_id(label) } }
%button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' }
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 380ab465bf4..094d6636c66 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -12,7 +12,7 @@
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author], field_name: "author_id", default_label: "Author" } })
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id].present?
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 539c4f3630a..210c9b9aab5 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -29,20 +29,21 @@
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.hide-collapsed
+ .value.hide-collapsed
- if issuable.assignee
- = link_to_member(@project, issuable.assignee, size: 32) do
+ = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
%span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle')
%span.username
= issuable.assignee.to_reference
- else
- %span.assign-yourself
+ %span.assign-yourself.no-value
No assignee
- if can_edit_issuable
+ \-
%a.js-assign-yourself{ href: '#' }
- \- assign yourself
+ assign yourself
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
@@ -62,13 +63,11 @@
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.hide-collapsed
+ .value.hide-collapsed
- if issuable.milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1}}
- = issuable.milestone.title
+ = link_to issuable.milestone.title, namespace_project_milestone_path(@project.namespace, @project, issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
- else
- .light None
+ %span.no-value None
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
@@ -85,14 +84,14 @@
= icon('spinner spin', class: 'block-loading')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.bold.hide-collapsed
+ .value.hide-collapsed
%span.value-content
- if issuable.due_date
- = issuable.due_date.to_s(:medium)
+ %span.bold= issuable.due_date.to_s(:medium)
- else
- None
+ %span.no-value No due date
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- %span.light.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) }
+ %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) }
\-
%a.js-remove-due-date{ href: "#", role: "button" }
remove due date
@@ -124,7 +123,7 @@
- issuable.labels_array.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
- .light None
+ %span.no-value None
.selectbox.hide-collapsed
- issuable.labels_array.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index c69d4cbfbe3..0191814849a 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,4 +1,5 @@
-- show_roles = local_assigns.fetch(:show_roles, true)
+- default_show_roles = can?(current_user, action_member_permission(:update, member), member) || can?(current_user, action_member_permission(:destroy, member), member)
+- show_roles = local_assigns.fetch(:show_roles, default_show_roles)
- show_controls = local_assigns.fetch(:show_controls, true)
- user = member.user
@@ -36,7 +37,7 @@
method: :post,
class: 'btn-xs btn'
- - if show_roles && can_see_member_roles?(source: member.source, user: current_user)
+ - if show_roles
%span.pull-right
%strong= member.human_access
- if show_controls
diff --git a/app/workers/gitlab_remove_project_export_worker.rb b/app/workers/gitlab_remove_project_export_worker.rb
new file mode 100644
index 00000000000..1d91897d520
--- /dev/null
+++ b/app/workers/gitlab_remove_project_export_worker.rb
@@ -0,0 +1,9 @@
+class GitlabRemoveProjectExportWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform
+ Project.remove_gitlab_exports!
+ end
+end
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
new file mode 100644
index 00000000000..39f6037e077
--- /dev/null
+++ b/app/workers/project_export_worker.rb
@@ -0,0 +1,12 @@
+class ProjectExportWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :gitlab_shell, retry: true
+
+ def perform(current_user_id, project_id)
+ current_user = User.find(current_user_id)
+ project = Project.find(project_id)
+
+ ::Projects::ImportExport::ExportService.new(project, current_user).execute
+ end
+end
diff --git a/bin/spring b/bin/spring
index 7fe232c3aae..e0d140fe0c7 100755
--- a/bin/spring
+++ b/bin/spring
@@ -3,7 +3,7 @@
# This file loads spring without using Bundler, in order to be fast.
# It gets overwritten when you run the `spring binstub` command.
-unless defined?(Spring)
+unless (defined?(Spring) || ENV['ENABLE_SPRING'] != '1') && File.basename($0) != 'spring'
require 'rubygems'
require 'bundler'
diff --git a/config/application.rb b/config/application.rb
index 49d4d3ba555..05fec995ed3 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -83,6 +83,7 @@ module Gitlab
config.assets.precompile << "mailers/*.css"
config.assets.precompile << "graphs/application.js"
config.assets.precompile << "users/application.js"
+ config.assets.precompile << "network/application.js"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 916fd33e767..09ffc319065 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -291,6 +291,9 @@ Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
+Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *'
+Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker'
#
# GitLab Shell
diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb
index 8fd27b1d88e..de2cdc6ecae 100644
--- a/config/initializers/default_url_options.rb
+++ b/config/initializers/default_url_options.rb
@@ -9,3 +9,4 @@ unless Gitlab.config.gitlab_on_standard_port?
end
Rails.application.routes.default_url_options = default_url_options
+ActionMailer::Base.asset_host = Settings.gitlab['base_url']
diff --git a/config/routes.rb b/config/routes.rb
index d52cbb22428..87da5e7178f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -171,6 +171,10 @@ Rails.application.routes.draw do
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
+
+ resource :gitlab_project, only: [:create, :new] do
+ post :create
+ end
end
#
@@ -348,6 +352,13 @@ Rails.application.routes.draw do
resources :keys
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
+
+ resources :personal_access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
+
resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
post :create_u2f
@@ -455,6 +466,10 @@ Rails.application.routes.draw do
post :housekeeping
post :toggle_star
post :markdown_preview
+ post :export
+ post :remove_export
+ post :generate_new_export
+ get :download_export
get :autocomplete_sources
get :activity
end
diff --git a/db/migrate/20160415062917_create_personal_access_tokens.rb b/db/migrate/20160415062917_create_personal_access_tokens.rb
new file mode 100644
index 00000000000..ce0b33f32bd
--- /dev/null
+++ b/db/migrate/20160415062917_create_personal_access_tokens.rb
@@ -0,0 +1,13 @@
+class CreatePersonalAccessTokens < ActiveRecord::Migration
+ def change
+ create_table :personal_access_tokens do |t|
+ t.references :user, index: true, foreign_key: true, null: false
+ t.string :token, index: { unique: true }, null: false
+ t.string :name, null: false
+ t.boolean :revoked, default: false
+ t.datetime :expires_at
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160616084004_change_project_of_environment.rb b/db/migrate/20160616084004_change_project_of_environment.rb
new file mode 100644
index 00000000000..cc1daf9b621
--- /dev/null
+++ b/db/migrate/20160616084004_change_project_of_environment.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ChangeProjectOfEnvironment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # 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
+ change_column_null :environments, :project_id, true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6a3be7297e3..5a27e9d5cdc 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160615142710) do
+ActiveRecord::Schema.define(version: 20160616084004) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -756,6 +756,19 @@ ActiveRecord::Schema.define(version: 20160615142710) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "personal_access_tokens", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "token", null: false
+ t.string "name", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "revoked", default: false
+ t.datetime "expires_at"
+ end
+
+ add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
+ add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
+
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
@@ -1095,5 +1108,6 @@ ActiveRecord::Schema.define(version: 20160615142710) do
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ add_foreign_key "personal_access_tokens", "users"
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/api/README.md b/doc/api/README.md
index e3fc5a09f21..71bb01e0d51 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -44,13 +44,11 @@ The following documentation is for the [internal CI API](ci/README.md):
## Authentication
-All API requests require authentication. You need to pass a `private_token`
-parameter via query string or header. If passed as a header, the header name
-must be `PRIVATE-TOKEN` (uppercase and with a dash instead of an underscore).
-You can find or reset your private token in your account page (`/profile/account`).
+All API requests require authentication via a token. There are three types of tokens
+available: private tokens, OAuth 2 tokens, and personal access tokens.
-If `private_token` is invalid or omitted, then an error message will be
-returned with status code `401`:
+If a token is invalid or omitted, an error message will be returned with
+status code `401`:
```json
{
@@ -58,42 +56,56 @@ returned with status code `401`:
}
```
-API requests should be prefixed with `api` and the API version. The API version
-is defined in [`lib/api.rb`][lib-api-url].
+### Private Tokens
-Example of a valid API request:
+You need to pass a `private_token` parameter via query string or header. If passed as a
+header, the header name must be `PRIVATE-TOKEN` (uppercase and with a dash instead of
+an underscore). You can find or reset your private token in your account page
+(`/profile/account`).
-```shell
-GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
-```
+### OAuth 2 Tokens
-Example of a valid API request using cURL and authentication via header:
+You can use an OAuth 2 token to authenticate with the API by passing it either in the
+`access_token` parameter or in the `Authorization` header.
+
+Example of using the OAuth2 token in the header:
```shell
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
```
-The API uses JSON to serialize data. You don't need to specify `.json` at the
-end of an API URL.
+Read more about [GitLab as an OAuth2 client](oauth2.md).
+
+### Personal Access Tokens
-## Authentication with OAuth2 token
+> **Note:** This feature was [introduced][ce-3749] in GitLab 8.8
-Instead of the `private_token` you can transmit the OAuth2 access token as a
-header or as a parameter.
+You can create as many personal access tokens as you like from your GitLab
+profile (`/profile/personal_access_tokens`); perhaps one for each application
+that needs access to the GitLab API.
-Example of OAuth2 token as a parameter:
+Once you have your token, pass it to the API using either the `private_token`
+parameter or the `PRIVATE-TOKEN` header.
+
+## Basic Usage
+
+API requests should be prefixed with `api` and the API version. The API version
+is defined in [`lib/api.rb`][lib-api-url].
+
+Example of a valid API request:
```shell
-curl https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
+GET https://gitlab.example.com/api/v3/projects?private_token=9koXpg98eAheJpvBs5tK
```
-Example of OAuth2 token as a header:
+Example of a valid API request using cURL and authentication via header:
```shell
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://example.com/api/v3/user
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
```
-Read more about [GitLab as an OAuth2 client](oauth2.md).
+The API uses JSON to serialize data. You don't need to specify `.json` at the
+end of an API URL.
## Status codes
@@ -330,3 +342,4 @@ programming languages. Visit the [GitLab website] for a complete list.
[GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API"
[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb
+[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
new file mode 100644
index 00000000000..ebd131c94ca
--- /dev/null
+++ b/doc/api/sidekiq_metrics.md
@@ -0,0 +1,152 @@
+# Sidekiq Metrics
+
+>**Note:** This endpoint is only available on GitLab 8.9 and above.
+
+This API endpoint allows you to retrieve some information about the current state
+of Sidekiq, its jobs, queues, and processes.
+
+## Get the current Queue Metrics
+
+List information about all the registered queues, their backlog and their
+latency.
+
+```
+GET /sidekiq/queue_metrics
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
+```
+
+Example response:
+
+```json
+{
+ "queues": {
+ "default": {
+ "backlog": 0,
+ "latency": 0
+ }
+ }
+}
+```
+
+## Get the current Process Metrics
+
+List information about all the Sidekiq workers registered to process your queues.
+
+```
+GET /sidekiq/process_metrics
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
+```
+
+Example response:
+
+```json
+{
+ "processes": [
+ {
+ "hostname": "gitlab.example.com",
+ "pid": 5649,
+ "tag": "gitlab",
+ "started_at": "2016-06-14T10:45:07.159-05:00",
+ "queues": [
+ "post_receive",
+ "mailers",
+ "archive_repo",
+ "system_hook",
+ "project_web_hook",
+ "gitlab_shell",
+ "incoming_email",
+ "runner",
+ "common",
+ "default"
+ ],
+ "labels": [],
+ "concurrency": 25,
+ "busy": 0
+ }
+ ]
+}
+```
+
+## Get the current Job Statistics
+
+List information about the jobs that Sidekiq has performed.
+
+```
+GET /sidekiq/job_stats
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
+```
+
+Example response:
+
+```json
+{
+ "jobs": {
+ "processed": 2,
+ "failed": 0,
+ "enqueued": 0
+ }
+}
+```
+
+## Get a compound response of all the previously mentioned metrics
+
+List all the currently available information about Sidekiq.
+
+```
+GET /sidekiq/compound_metrics
+```
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
+```
+
+Example response:
+
+```json
+{
+ "queues": {
+ "default": {
+ "backlog": 0,
+ "latency": 0
+ }
+ },
+ "processes": [
+ {
+ "hostname": "gitlab.example.com",
+ "pid": 5649,
+ "tag": "gitlab",
+ "started_at": "2016-06-14T10:45:07.159-05:00",
+ "queues": [
+ "post_receive",
+ "mailers",
+ "archive_repo",
+ "system_hook",
+ "project_web_hook",
+ "gitlab_shell",
+ "incoming_email",
+ "runner",
+ "common",
+ "default"
+ ],
+ "labels": [],
+ "concurrency": 25,
+ "busy": 0
+ }
+ ],
+ "jobs": {
+ "processed": 2,
+ "failed": 0,
+ "enqueued": 0
+ }
+}
+```
+
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 6cd9b274d11..c2272ab0a2b 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -94,23 +94,8 @@ Visibility: public
Number of lines: 21
def #{name}(#{args_signature})
- trans = Gitlab::Metrics::Instrumentation.transaction
-
- if trans
- start = Time.now
- cpu_start = Gitlab::Metrics::System.cpu_time
- retval = super
- duration = (Time.now - start) * 1000.0
-
- if duration >= Gitlab::Metrics.method_call_threshold
- cpu_duration = Gitlab::Metrics::System.cpu_time - cpu_start
-
- trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
- { duration: duration, cpu_duration: cpu_duration },
- method: #{label.inspect})
- end
-
- retval
+ if trans = Gitlab::Metrics::Instrumentation.transaction
+ trans.measure_method(#{label.inspect}) { super }
else
super
end
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 02e024ca15a..8a7547e5322 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -34,6 +34,15 @@ First, you need to provide information on whether the migration can be applied:
3. online with errors on new instances while migrating
4. offline (needs to happen without app servers to prevent db corruption)
+For example:
+
+```
+# rubocop:disable all
+# Migration type: online without errors (works on previous version and new one)
+class MyMigration < ActiveRecord::Migration
+...
+```
+
It is always preferable to have a migration run online. If you expect the migration
to take particularly long (for instance, if it loops through all notes),
this is valuable information to add.
@@ -48,7 +57,6 @@ be possible to downgrade in case of a vulnerability or bugs.
In your migration, add a comment describing how the reversibility of the
migration was tested.
-
## Removing indices
If you need to remove index, please add a condition like in following example:
@@ -70,6 +78,7 @@ so:
```
class MyMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def change
@@ -90,8 +99,11 @@ value of `10` you'd write the following:
```
class MyMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
def up
- add_column_with_default(:projects, :foo, :integer, 10)
+ add_column_with_default(:projects, :foo, :integer, default: 10)
end
def down
diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature
index 5de07e90e28..f5bb06dea7d 100644
--- a/features/admin/active_tab.feature
+++ b/features/admin/active_tab.feature
@@ -5,28 +5,36 @@ Feature: Admin Active Tab
Scenario: On Admin Home
Given I visit admin page
- Then the active main tab should be Home
+ Then the active main tab should be Overview
And no other main tabs should be active
Scenario: On Admin Projects
Given I visit admin projects page
- Then the active main tab should be Projects
+ Then the active main tab should be Overview
+ And the active sub tab should be Projects
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Groups
Given I visit admin groups page
- Then the active main tab should be Groups
+ Then the active main tab should be Overview
+ And the active sub tab should be Groups
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Users
Given I visit admin users page
- Then the active main tab should be Users
+ Then the active main tab should be Overview
+ And the active sub tab should be Users
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Logs
Given I visit admin logs page
- Then the active main tab should be Logs
+ Then the active main tab should be Monitoring
+ And the active sub tab should be Logs
And no other main tabs should be active
+ And no other sub tabs should be active
Scenario: On Admin Messages
Given I visit admin messages page
@@ -40,5 +48,7 @@ Feature: Admin Active Tab
Scenario: On Admin Resque
Given I visit admin Resque page
- Then the active main tab should be Resque
+ Then the active main tab should be Monitoring
+ And the active sub tab should be Resque
And no other main tabs should be active
+ And no other sub tabs should be active
diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature
index 76392068357..56b4a639c01 100644
--- a/features/dashboard/new_project.feature
+++ b/features/dashboard/new_project.feature
@@ -14,7 +14,7 @@ Background:
@javascript
Scenario: I should see instructions on how to import from Git URL
Given I see "New Project" page
- When I click on "Any repo by URL"
+ When I click on "Repo by URL"
Then I see instructions on how to import from Git URL
@javascript
diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb
index f2db1801389..9b1689a8198 100644
--- a/features/steps/admin/active_tab.rb
+++ b/features/steps/admin/active_tab.rb
@@ -1,45 +1,41 @@
class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
- include SharedSidebarActiveTab
+ include SharedActiveTab
- step 'the active main tab should be Home' do
+ step 'the active main tab should be Overview' do
ensure_active_main_tab('Overview')
end
- step 'the active main tab should be Projects' do
- ensure_active_main_tab('Projects')
+ step 'the active sub tab should be Projects' do
+ ensure_active_sub_tab('Projects')
end
- step 'the active main tab should be Groups' do
- ensure_active_main_tab('Groups')
+ step 'the active sub tab should be Groups' do
+ ensure_active_sub_tab('Groups')
end
- step 'the active main tab should be Users' do
- ensure_active_main_tab('Users')
- end
-
- step 'the active main tab should be Logs' do
- ensure_active_main_tab('Logs')
+ step 'the active sub tab should be Users' do
+ ensure_active_sub_tab('Users')
end
step 'the active main tab should be Hooks' do
ensure_active_main_tab('Hooks')
end
- step 'the active main tab should be Resque' do
- ensure_active_main_tab('Background Jobs')
+ step 'the active main tab should be Monitoring' do
+ ensure_active_main_tab('Monitoring')
end
- step 'the active main tab should be Messages' do
- ensure_active_main_tab('Messages')
+ step 'the active sub tab should be Resque' do
+ ensure_active_sub_tab('Background Jobs')
end
- step 'no other main tabs should be active' do
- expect(page).to have_selector('.nav-sidebar > li.active', count: 1)
+ step 'the active sub tab should be Logs' do
+ ensure_active_sub_tab('Logs')
end
- def ensure_active_main_tab(content)
- expect(find('.nav-sidebar > li.active')).to have_content(content)
+ step 'the active main tab should be Messages' do
+ ensure_active_main_tab('Messages')
end
end
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index a0aad66184d..29e6b9f1a01 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -10,7 +10,8 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
end
step 'I see "New Project" page' do
- expect(page).to have_content('Project path')
+ expect(page).to have_content('Project owner')
+ expect(page).to have_content('Project name')
end
step 'I see all possible import optios' do
@@ -19,7 +20,8 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
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('Any repo by URL')
+ 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
@@ -36,7 +38,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
end
end
- step 'I click on "Any repo by URL"' do
+ step 'I click on "Repo by URL"' do
first('.import_git').click
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 2c0498de3b9..79a3ed8197e 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -202,8 +202,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I see Browse dir link' do
- expect(page).to have_link 'Browse Directory »'
- expect(page).not_to have_link 'Browse Code »'
+ expect(page).to have_link 'Browse Directory'
+ expect(page).not_to have_link 'Browse Code'
end
step 'I click on readme file' do
@@ -219,7 +219,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I see Browse code link' do
expect(page).to have_link 'Browse Files'
- expect(page).not_to have_link 'Browse Directory »'
+ expect(page).not_to have_link 'Browse Directory'
end
step 'I click on Permalink' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6cd909f6115..51ddd0dbfc4 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -59,5 +59,6 @@ module API
mount ::API::Licenses
mount ::API::Subscriptions
mount ::API::Gitignores
+ mount ::API::SidekiqMetrics
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index de5959e3aae..77e407b54c5 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -9,9 +9,13 @@ module API
[ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
end
+ def find_user_by_private_token
+ token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+ User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
+ end
+
def current_user
- private_token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
- @current_user ||= (User.find_by(authentication_token: private_token) || doorkeeper_guard)
+ @current_user ||= (find_user_by_private_token || doorkeeper_guard)
unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
return nil
@@ -33,7 +37,7 @@ module API
identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
# Regex for integers
- if !!(identifier =~ /^[0-9]+$/)
+ if !!(identifier =~ /\A[0-9]+\z/)
identifier.to_i
else
identifier
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
new file mode 100644
index 00000000000..d3d6827dc54
--- /dev/null
+++ b/lib/api/sidekiq_metrics.rb
@@ -0,0 +1,90 @@
+require 'sidekiq/api'
+
+module API
+ class SidekiqMetrics < Grape::API
+ before { authenticated_as_admin! }
+
+ helpers do
+ def queue_metrics
+ Sidekiq::Queue.all.each_with_object({}) do |queue, hash|
+ hash[queue.name] = {
+ backlog: queue.size,
+ latency: queue.latency.to_i
+ }
+ end
+ end
+
+ def process_metrics
+ Sidekiq::ProcessSet.new.map do |process|
+ {
+ hostname: process['hostname'],
+ pid: process['pid'],
+ tag: process['tag'],
+ started_at: Time.at(process['started_at']),
+ queues: process['queues'],
+ labels: process['labels'],
+ concurrency: process['concurrency'],
+ busy: process['busy']
+ }
+ end
+ end
+
+ def job_stats
+ stats = Sidekiq::Stats.new
+ {
+ processed: stats.processed,
+ failed: stats.failed,
+ enqueued: stats.enqueued
+ }
+ end
+ end
+
+ # Get Sidekiq Queue metrics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/queue_metrics
+ #
+ get 'sidekiq/queue_metrics' do
+ { queues: queue_metrics }
+ end
+
+ # Get Sidekiq Process metrics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/process_metrics
+ #
+ get 'sidekiq/process_metrics' do
+ { processes: process_metrics }
+ end
+
+ # Get Sidekiq Job statistics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/job_stats
+ #
+ get 'sidekiq/job_stats' do
+ { jobs: job_stats }
+ end
+
+ # Get Sidekiq Compound metrics. Includes all previous metrics
+ #
+ # Parameters:
+ # None
+ #
+ # Example:
+ # GET /sidekiq/compound_metrics
+ #
+ get 'sidekiq/compound_metrics' do
+ { queues: queue_metrics, processes: process_metrics, jobs: job_stats }
+ end
+ end
+end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index db95d7c908b..4815bafe238 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -103,7 +103,7 @@ module Banzai
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
- each_node do |node|
+ nodes.each do |node|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content|
object_link_filter(content, ref_pattern)
@@ -206,6 +206,55 @@ module Banzai
text
end
+ # Returns a Hash containing all object references (e.g. issue IDs) per the
+ # project they belong to.
+ def references_per_project
+ @references_per_project ||= begin
+ refs = Hash.new { |hash, key| hash[key] = Set.new }
+
+ regex = Regexp.union(object_class.reference_pattern,
+ object_class.link_reference_pattern)
+
+ nodes.each do |node|
+ node.to_html.scan(regex) do
+ project = $~[:project] || current_project_path
+
+ refs[project] << $~[object_sym]
+ end
+ end
+
+ refs
+ end
+ end
+
+ # Returns a Hash containing referenced projects grouped per their full
+ # path.
+ def projects_per_reference
+ @projects_per_reference ||= begin
+ hash = {}
+ refs = Set.new
+
+ references_per_project.each do |project_ref, _|
+ refs << project_ref
+ end
+
+ find_projects_for_paths(refs.to_a).each do |project|
+ hash[project.path_with_namespace] = project
+ end
+
+ hash
+ end
+ end
+
+ # Returns the projects for the given paths.
+ def find_projects_for_paths(paths)
+ Project.where_paths_in(paths).includes(:namespace)
+ end
+
+ def current_project_path
+ @current_project_path ||= project.path_with_namespace
+ end
+
private
def project_refs_cache
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index f73ecfc9418..0a29c547a4d 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -3,17 +3,8 @@ module Banzai
# HTML Filter to modify the attributes of external links
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
- doc.search('a').each do |node|
- link = node.attr('href')
-
- next unless link
-
- # Skip non-HTTP(S) links
- next unless link.start_with?('http')
-
- # Skip internal links
- next if link.start_with?(internal_url)
-
+ # Skip non-HTTP(S) links and internal links
+ doc.xpath("descendant-or-self::a[starts-with(@href, 'http') and not(starts-with(@href, '#{internal_url}'))]").each do |node|
node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 2496e704002..2614261f9eb 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -11,13 +11,40 @@ module Banzai
Issue
end
- def find_object(project, id)
- project.get_issue(id)
+ def find_object(project, iid)
+ issues_per_project[project][iid]
end
def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
end
+
+ def project_from_ref(ref)
+ projects_per_reference[ref || current_project_path]
+ end
+
+ # Returns a Hash containing the issues per Project instance.
+ def issues_per_project
+ @issues_per_project ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ projects_per_reference.each do |path, project|
+ issue_ids = references_per_project[path]
+
+ next unless project.default_issues_tracker?
+
+ project.issues.where(iid: issue_ids.to_a).each do |issue|
+ hash[project][issue.iid] = issue
+ end
+ end
+
+ hash
+ end
+ end
+
+ def find_projects_for_paths(paths)
+ super(paths).includes(:gitlab_issue_tracker_service)
+ end
end
end
end
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index c0f503c9af3..45bb66dc99f 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -10,11 +10,11 @@ module Banzai
def call
return doc unless project
- doc.search('a').each do |el|
+ doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el|
process_link_attr el.attribute('href')
end
- doc.search('img').each do |el|
+ doc.xpath('descendant-or-self::img[starts-with(@src, "/uploads/")]').each do |el|
process_link_attr el.attribute('src')
end
@@ -24,12 +24,7 @@ module Banzai
protected
def process_link_attr(html_attr)
- return if html_attr.blank?
-
- uri = html_attr.value
- if uri.starts_with?("/uploads/")
- html_attr.value = build_url(uri).to_s
- end
+ html_attr.value = build_url(html_attr.value).to_s
end
def build_url(uri)
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 68246497e90..ed86de819eb 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -30,7 +30,10 @@ module Ci
end
def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
- builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)}
+ builds.select do |build|
+ build[:stage] == stage &&
+ process?(build[:only], build[:except], ref, tag, trigger_request)
+ end
end
def builds
@@ -51,7 +54,7 @@ module Ci
job = @jobs[name.to_sym]
return [] unless job
- job.fetch(:variables, [])
+ job[:variables] || []
end
private
@@ -201,12 +204,12 @@ module Ci
raise ValidationError, "#{name} job: tags parameter should be an array of strings"
end
- if job[:only] && !validate_array_of_strings(job[:only])
- raise ValidationError, "#{name} job: only parameter should be an array of strings"
+ if job[:only] && !validate_array_of_strings_or_regexps(job[:only])
+ raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps"
end
- if job[:except] && !validate_array_of_strings(job[:except])
- raise ValidationError, "#{name} job: except parameter should be an array of strings"
+ if job[:except] && !validate_array_of_strings_or_regexps(job[:except])
+ raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps"
end
if job[:allow_failure] && !validate_boolean(job[:allow_failure])
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index e0b3f14d384..42232b7129d 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -15,11 +15,11 @@ module ContainerRegistry
end
def repository_tags(name)
- @faraday.get("/v2/#{name}/tags/list").body
+ response_body @faraday.get("/v2/#{name}/tags/list")
end
def repository_manifest(name, reference)
- @faraday.get("/v2/#{name}/manifests/#{reference}").body
+ response_body @faraday.get("/v2/#{name}/manifests/#{reference}")
end
def repository_tag_digest(name, reference)
@@ -34,7 +34,7 @@ module ContainerRegistry
def blob(name, digest, type = nil)
headers = {}
headers['Accept'] = type if type
- @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers).body
+ response_body @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers)
end
def delete_blob(name, digest)
@@ -47,6 +47,7 @@ module ContainerRegistry
conn.request :json
conn.headers['Accept'] = MANIFEST_VERSION
+ conn.response :json, content_type: 'application/json'
conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+prettyjws'
conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+json'
conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v2+json'
@@ -59,5 +60,9 @@ module ContainerRegistry
conn.adapter :net_http
end
+
+ def response_body(response)
+ response.body if response.success?
+ end
end
end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index adbf5941a96..7e3f5abba62 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -1,5 +1,3 @@
-require_relative 'shell_env'
-
module Grack
class AuthSpawner
def self.call(env)
@@ -61,11 +59,6 @@ module Grack
end
@user = authenticate_user(login, password)
-
- if @user
- Gitlab::ShellEnv.set_env(@user)
- @env['REMOTE_USER'] = @auth.username
- end
end
def ci_request?(login, password)
diff --git a/lib/gitlab/backend/shell_env.rb b/lib/gitlab/backend/shell_env.rb
deleted file mode 100644
index 9f5adee594a..00000000000
--- a/lib/gitlab/backend/shell_env.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module Gitlab
- # This module provide 2 methods
- # to set specific ENV variables for GitLab Shell
- module ShellEnv
- extend self
-
- def set_env(user)
- # Set GL_ID env variable
- if user
- ENV['GL_ID'] = gl_id(user)
- end
- end
-
- def reset_env
- # Reset GL_ID env variable
- ENV['GL_ID'] = nil
- end
-
- def gl_id(user)
- if user.present?
- "user-#{user.id}"
- else
- # This empty string is used in the render_grack_auth_ok method
- ""
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/validation_helpers.rb b/lib/gitlab/ci/config/node/validation_helpers.rb
index 3900fc89391..72f648975dc 100644
--- a/lib/gitlab/ci/config/node/validation_helpers.rb
+++ b/lib/gitlab/ci/config/node/validation_helpers.rb
@@ -15,6 +15,10 @@ module Gitlab
values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
+ end
+
def validate_variables(variables)
variables.is_a?(Hash) &&
variables.all? { |key, value| validate_string(key) && validate_string(value) }
@@ -24,6 +28,19 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
+ def validate_string_or_regexp(value)
+ return true if value.is_a?(Symbol)
+ return false unless value.is_a?(String)
+
+ if value.first == '/' && value.last == '/'
+ Regexp.new(value[1...-1])
+ else
+ true
+ end
+ rescue RegexpError
+ false
+ end
+
def validate_environment(value)
value.is_a?(String) && value =~ Gitlab::Regex.environment_name_regex
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 5e7532f57ae..28c34429c1f 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -36,7 +36,7 @@ module Gitlab
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
- import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
+ import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index dd3ff0ab18b..dec20d8659b 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -28,65 +28,79 @@ module Gitlab
# Updates the value of a column in batches.
#
# This method updates the table in batches of 5% of the total row count.
- # Any data inserted while running this method (or after it has finished
- # running) is _not_ updated automatically.
+ # This method will continue updating rows until no rows remain.
+ #
+ # When given a block this method will yield two values to the block:
+ #
+ # 1. An instance of `Arel::Table` for the table that is being updated.
+ # 2. The query to run as an Arel object.
+ #
+ # By supplying a block one can add extra conditions to the queries being
+ # executed. Note that the same block is used for _all_ queries.
+ #
+ # Example:
+ #
+ # update_column_in_batches(:projects, :foo, 10) do |table, query|
+ # query.where(table[:some_column].eq('hello'))
+ # end
+ #
+ # This would result in this method updating only rows where
+ # `projects.some_column` equals "hello".
#
# table - The name of the table.
# column - The name of the column to update.
# value - The value for the column.
+ #
+ # Rubocop's Metrics/AbcSize metric is disabled for this method as Rubocop
+ # determines this method to be too complex while there's no way to make it
+ # less "complex" without introducing extra methods (which actually will
+ # make things _more_ complex).
+ #
+ # rubocop: disable Metrics/AbcSize
def update_column_in_batches(table, column, value)
- quoted_table = quote_table_name(table)
- quoted_column = quote_column_name(column)
-
- ##
- # Workaround for #17711
- #
- # It looks like for MySQL `ActiveRecord::Base.conntection.quote(true)`
- # returns correct value (1), but `ActiveRecord::Migration.new.quote`
- # returns incorrect value ('true'), which causes migrations to fail.
- #
- quoted_value = connection.quote(value)
- processed = 0
-
- total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}").
- to_hash.
- first['count'].
- to_i
+ table = Arel::Table.new(table)
+
+ count_arel = table.project(Arel.star.count.as('count'))
+ count_arel = yield table, count_arel if block_given?
+
+ total = exec_query(count_arel.to_sql).to_hash.first['count'].to_i
+
+ return if total == 0
# Update in batches of 5% until we run out of any rows to update.
batch_size = ((total / 100.0) * 5.0).ceil
+ start_arel = table.project(table[:id]).order(table[:id].asc).take(1)
+ start_arel = yield table, start_arel if block_given?
+ start_id = exec_query(start_arel.to_sql).to_hash.first['id'].to_i
+
loop do
- start_row = exec_query(%Q{
- SELECT id
- FROM #{quoted_table}
- ORDER BY id ASC
- LIMIT 1 OFFSET #{processed}
- }).to_hash.first
-
- # There are no more rows to process
- break unless start_row
-
- stop_row = exec_query(%Q{
- SELECT id
- FROM #{quoted_table}
- ORDER BY id ASC
- LIMIT 1 OFFSET #{processed + batch_size}
- }).to_hash.first
-
- query = %Q{
- UPDATE #{quoted_table}
- SET #{quoted_column} = #{quoted_value}
- WHERE id >= #{start_row['id']}
- }
+ stop_arel = table.project(table[:id]).
+ where(table[:id].gteq(start_id)).
+ order(table[:id].asc).
+ take(1).
+ skip(batch_size)
+
+ stop_arel = yield table, stop_arel if block_given?
+ stop_row = exec_query(stop_arel.to_sql).to_hash.first
+
+ update_arel = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(table).
+ set([[table[column], value]]).
+ where(table[:id].gteq(start_id))
if stop_row
- query += " AND id < #{stop_row['id']}"
+ stop_id = stop_row['id'].to_i
+ start_id = stop_id
+ update_arel = update_arel.where(table[:id].lt(stop_id))
end
- execute(query)
+ update_arel = yield table, update_arel if block_given?
+
+ execute(update_arel.to_sql)
- processed += batch_size
+ # There are no more rows left to update.
+ break unless stop_row
end
end
@@ -95,9 +109,9 @@ module Gitlab
# This method runs the following steps:
#
# 1. Add the column with a default value of NULL.
- # 2. Update all existing rows in batches.
- # 3. Change the default value of the column to the specified value.
- # 4. Update any remaining rows.
+ # 2. Change the default value of the column to the specified value.
+ # 3. Update all existing rows in batches.
+ # 4. Set a `NOT NULL` constraint on the column if desired (the default).
#
# These steps ensure a column can be added to a large and commonly used
# table without locking the entire table for the duration of the table
@@ -109,7 +123,10 @@ module Gitlab
# default - The default value for the column.
# allow_null - When set to `true` the column will allow NULL values, the
# default is to not allow NULL values.
- def add_column_with_default(table, column, type, default:, allow_null: false)
+ #
+ # This method can also take a block which is passed directly to the
+ # `update_column_in_batches` method.
+ def add_column_with_default(table, column, type, default:, allow_null: false, &block)
if transaction_open?
raise 'add_column_with_default can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
@@ -125,11 +142,9 @@ module Gitlab
end
begin
- transaction do
- update_column_in_batches(table, column, default)
+ update_column_in_batches(table, column, default, &block)
- change_column_null(table, column, false) unless allow_null
- end
+ change_column_null(table, column, false) unless allow_null
# We want to rescue _all_ exceptions here, even those that don't inherit
# from StandardError.
rescue Exception => error # rubocop: disable all
diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb
index 77c33db4b59..3d0418261bb 100644
--- a/lib/gitlab/gitlab_import/project_creator.rb
+++ b/lib/gitlab/gitlab_import/project_creator.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(
+ ::Projects::CreateService.new(
current_user,
name: repo["name"],
path: repo["path"],
@@ -22,8 +22,6 @@ module Gitlab
import_source: repo["path_with_namespace"],
import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@")
).execute
-
- project
end
end
end
diff --git a/lib/gitlab/gl_id.rb b/lib/gitlab/gl_id.rb
new file mode 100644
index 00000000000..624fd00367e
--- /dev/null
+++ b/lib/gitlab/gl_id.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module GlId
+ def self.gl_id(user)
+ if user.present?
+ "user-#{user.id}"
+ else
+ ""
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
new file mode 100644
index 00000000000..624c1766024
--- /dev/null
+++ b/lib/gitlab/import_export.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module ImportExport
+ extend self
+
+ VERSION = '0.1.0'
+
+ def export_path(relative_path:)
+ File.join(storage_path, relative_path)
+ end
+
+ def storage_path
+ File.join(Settings.shared['path'], 'tmp/project_exports')
+ end
+
+ def project_filename
+ "project.json"
+ end
+
+ def project_bundle_filename
+ "project.bundle"
+ end
+
+ def config_file
+ 'lib/gitlab/import_export/import_export.yml'
+ end
+
+ def version_filename
+ 'VERSION'
+ end
+
+ def version
+ VERSION
+ end
+
+ def reset_tokens?
+ true
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
new file mode 100644
index 00000000000..d230de781d5
--- /dev/null
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module ImportExport
+ class AttributesFinder
+
+ def initialize(included_attributes:, excluded_attributes:, methods:)
+ @included_attributes = included_attributes || {}
+ @excluded_attributes = excluded_attributes || {}
+ @methods = methods || {}
+ end
+
+ def find(model_object)
+ parsed_hash = find_attributes_only(model_object)
+ parsed_hash.empty? ? model_object : { model_object => parsed_hash }
+ end
+
+ def parse(model_object)
+ parsed_hash = find_attributes_only(model_object)
+ yield parsed_hash unless parsed_hash.empty?
+ end
+
+ def find_included(value)
+ key = key_from_hash(value)
+ @included_attributes[key].nil? ? {} : { only: @included_attributes[key] }
+ end
+
+ def find_excluded(value)
+ key = key_from_hash(value)
+ @excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] }
+ end
+
+ def find_method(value)
+ key = key_from_hash(value)
+ @methods[key].nil? ? {} : { methods: @methods[key] }
+ end
+
+ private
+
+ def find_attributes_only(value)
+ find_included(value).merge(find_excluded(value)).merge(find_method(value))
+ end
+
+ def key_from_hash(value)
+ value.is_a?(Hash) ? value.keys.first : value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
new file mode 100644
index 00000000000..78664f076eb
--- /dev/null
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module ImportExport
+ module CommandLineUtil
+ def tar_czf(archive:, dir:)
+ tar_with_options(archive: archive, dir: dir, options: 'czf')
+ end
+
+ def untar_zxf(archive:, dir:)
+ untar_with_options(archive: archive, dir: dir, options: 'zxf')
+ end
+
+ def git_bundle(repo_path:, bundle_path:)
+ execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
+ end
+
+ def git_unbundle(repo_path:, bundle_path:)
+ execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
+ end
+
+ private
+
+ def tar_with_options(archive:, dir:, options:)
+ execute(%W(tar -#{options} #{archive} -C #{dir} .))
+ end
+
+ def untar_with_options(archive:, dir:, options:)
+ execute(%W(tar -#{options} #{archive} -C #{dir}))
+ end
+
+ def execute(cmd)
+ _output, status = Gitlab::Popen.popen(cmd)
+ status.zero?
+ end
+
+ def git_bin_path
+ Gitlab.config.git.bin_path
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/error.rb b/lib/gitlab/import_export/error.rb
new file mode 100644
index 00000000000..e341c4d9cf8
--- /dev/null
+++ b/lib/gitlab/import_export/error.rb
@@ -0,0 +1,5 @@
+module Gitlab
+ module ImportExport
+ class Error < StandardError; end
+ end
+end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
new file mode 100644
index 00000000000..0e70d9282d5
--- /dev/null
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module ImportExport
+ class FileImporter
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def self.import(*args)
+ new(*args).import
+ end
+
+ def initialize(archive_file:, shared:)
+ @archive_file = archive_file
+ @shared = shared
+ end
+
+ def import
+ FileUtils.mkdir_p(@shared.export_path)
+ decompress_archive
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def decompress_archive
+ untar_zxf(archive: @archive_file, dir: @shared.export_path)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
new file mode 100644
index 00000000000..164ab6238c4
--- /dev/null
+++ b/lib/gitlab/import_export/import_export.yml
@@ -0,0 +1,54 @@
+# Model relationships to be included in the project import/export
+project_tree:
+ - issues:
+ - notes:
+ :author
+ - :labels
+ - :milestones
+ - snippets:
+ - notes:
+ :author
+ - :releases
+ - :events
+ - project_members:
+ - :user
+ - merge_requests:
+ - notes:
+ :author
+ - :merge_request_diff
+ - pipelines:
+ - notes:
+ :author
+ - :statuses
+ - :variables
+ - :triggers
+ - :deploy_keys
+ - :services
+ - :hooks
+ - :protected_branches
+
+# Only include the following attributes for the models specified.
+included_attributes:
+ project:
+ - :description
+ - :issues_enabled
+ - :merge_requests_enabled
+ - :wiki_enabled
+ - :snippets_enabled
+ - :visibility_level
+ - :archived
+ user:
+ - :id
+ - :email
+ - :username
+ author:
+ - :name
+
+# Do not include the following attributes for the models specified.
+excluded_attributes:
+ snippets:
+ - :expired_at
+
+methods:
+ statuses:
+ - :type \ No newline at end of file
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
new file mode 100644
index 00000000000..d209e04f7be
--- /dev/null
+++ b/lib/gitlab/import_export/importer.rb
@@ -0,0 +1,64 @@
+module Gitlab
+ module ImportExport
+ class Importer
+
+ def initialize(project)
+ @archive_file = project.import_source
+ @current_user = project.creator
+ @project = project
+ @shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace)
+ end
+
+ def execute
+ Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file,
+ shared: @shared)
+ if check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
+ project_tree.restored_project
+ else
+ raise Projects::ImportService::Error.new(@shared.errors.join(', '))
+ end
+ end
+
+ private
+
+ def check_version!
+ Gitlab::ImportExport::VersionChecker.check!(shared: @shared)
+ end
+
+ def project_tree
+ @project_tree ||= Gitlab::ImportExport::ProjectTreeRestorer.new(user: @current_user,
+ shared: @shared,
+ project: @project)
+ end
+
+ def repo_restorer
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
+ shared: @shared,
+ project: project_tree.restored_project)
+ end
+
+ def wiki_restorer
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
+ shared: @shared,
+ project: ProjectWiki.new(project_tree.restored_project),
+ wiki: true)
+ end
+
+ def uploads_restorer
+ Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: @shared)
+ end
+
+ def path_with_namespace
+ File.join(@project.namespace.path, @project.path)
+ end
+
+ def repo_path
+ File.join(@shared.export_path, 'project.bundle')
+ end
+
+ def wiki_repo_path
+ File.join(@shared.export_path, 'project.wiki.bundle')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
new file mode 100644
index 00000000000..c569a35a48b
--- /dev/null
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -0,0 +1,68 @@
+module Gitlab
+ module ImportExport
+ class MembersMapper
+
+ attr_reader :missing_author_ids
+
+ def initialize(exported_members:, user:, project:)
+ @exported_members = exported_members
+ @user = user
+ @project = project
+ @missing_author_ids = []
+
+ # This needs to run first, as second call would be from #map
+ # which means project members already exist.
+ ensure_default_member!
+ end
+
+ def map
+ @map ||=
+ begin
+ @exported_members.inject(missing_keys_tracking_hash) do |hash, member|
+ existing_user = User.where(find_project_user_query(member)).first
+ old_user_id = member['user']['id']
+ if existing_user && add_user_as_team_member(existing_user, member)
+ hash[old_user_id] = existing_user.id
+ end
+ hash
+ end
+ end
+ end
+
+ def default_user_id
+ @user.id
+ end
+
+ private
+
+ def missing_keys_tracking_hash
+ Hash.new do |_, key|
+ @missing_author_ids << key
+ default_user_id
+ end
+ end
+
+ def ensure_default_member!
+ ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
+ end
+
+ def add_user_as_team_member(existing_user, member)
+ member['user'] = existing_user
+
+ ProjectMember.create(member_hash(member)).persisted?
+ end
+
+ def member_hash(member)
+ member.except('id').merge(source_id: @project.id, importing: true)
+ end
+
+ def find_project_user_query(member)
+ user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email']))
+ end
+
+ def user_arel
+ @user_arel ||= User.arel_table
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_creator.rb b/lib/gitlab/import_export/project_creator.rb
new file mode 100644
index 00000000000..89388d1984b
--- /dev/null
+++ b/lib/gitlab/import_export/project_creator.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module ImportExport
+ class ProjectCreator
+
+ def initialize(namespace_id, current_user, file, project_path)
+ @namespace_id = namespace_id
+ @current_user = current_user
+ @file = file
+ @project_path = project_path
+ end
+
+ def execute
+ ::Projects::CreateService.new(
+ @current_user,
+ name: @project_path,
+ path: @project_path,
+ namespace_id: @namespace_id,
+ import_type: "gitlab_project",
+ import_source: @file
+ ).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
new file mode 100644
index 00000000000..dd71b92c522
--- /dev/null
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -0,0 +1,105 @@
+module Gitlab
+ module ImportExport
+ class ProjectTreeRestorer
+
+ def initialize(user:, shared:, project:)
+ @path = File.join(shared.export_path, 'project.json')
+ @user = user
+ @shared = shared
+ @project = project
+ end
+
+ def restore
+ json = IO.read(@path)
+ @tree_hash = ActiveSupport::JSON.decode(json)
+ @project_members = @tree_hash.delete('project_members')
+ create_relations
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ def restored_project
+ @restored_project ||= restore_project
+ end
+
+ private
+
+ def members_mapper
+ @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members,
+ user: @user,
+ project: restored_project)
+ end
+
+ # Loops through the tree of models defined in import_export.yml and
+ # finds them in the imported JSON so they can be instantiated and saved
+ # in the DB. The structure and relationships between models are guessed from
+ # the configuration yaml file too.
+ # Finally, it updates each attribute in the newly imported project.
+ def create_relations
+ saved = []
+ default_relation_list.each do |relation|
+ next unless relation.is_a?(Hash) || @tree_hash[relation.to_s].present?
+
+ create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash)
+
+ relation_key = relation.is_a?(Hash) ? relation.keys.first : relation
+ relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s])
+ saved << restored_project.update_attribute(relation_key, relation_hash)
+ end
+ saved.all?
+ end
+
+ def default_relation_list
+ Gitlab::ImportExport::Reader.new(shared: @shared).tree.reject do |model|
+ model.is_a?(Hash) && model[:project_members]
+ end
+ end
+
+ def restore_project
+ return @project unless @tree_hash
+
+ project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
+ @project.update(project_params)
+ @project
+ end
+
+ # Given a relation hash containing one or more models and its relationships,
+ # loops through each model and each object from a model type and
+ # and assigns its correspondent attributes hash from +tree_hash+
+ # Example:
+ # +relation_key+ issues, loops through the list of *issues* and for each individual
+ # issue, finds any subrelations such as notes, creates them and assign them back to the hash
+ def create_sub_relations(relation, tree_hash)
+ relation_key = relation.keys.first.to_s
+ tree_hash[relation_key].each do |relation_item|
+ relation.values.flatten.each do |sub_relation|
+ relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation)
+ relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank?
+ end
+ end
+ end
+
+ def assign_relation_hash(relation_item, sub_relation)
+ if sub_relation.is_a?(Hash)
+ relation_hash = relation_item[sub_relation.keys.first.to_s]
+ sub_relation = sub_relation.keys.first
+ else
+ relation_hash = relation_item[sub_relation.to_s]
+ end
+ [relation_hash, sub_relation]
+ end
+
+ def create_relation(relation, relation_hash_list)
+ relation_array = [relation_hash_list].flatten.map do |relation_hash|
+ Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
+ relation_hash: relation_hash.merge('project_id' => restored_project.id),
+ members_mapper: members_mapper,
+ user: @user)
+ end
+
+ relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
new file mode 100644
index 00000000000..9153088e966
--- /dev/null
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module ImportExport
+ class ProjectTreeSaver
+ attr_reader :full_path
+
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ @full_path = File.join(@shared.export_path, ImportExport.project_filename)
+ end
+
+ def save
+ FileUtils.mkdir_p(@shared.export_path)
+
+ File.write(full_path, project_json_tree)
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def project_json_tree
+ @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
new file mode 100644
index 00000000000..19defd8f03a
--- /dev/null
+++ b/lib/gitlab/import_export/reader.rb
@@ -0,0 +1,117 @@
+module Gitlab
+ module ImportExport
+ class Reader
+
+ attr_reader :tree
+
+ def initialize(shared:)
+ @shared = shared
+ config_hash = YAML.load_file(Gitlab::ImportExport.config_file).deep_symbolize_keys
+ @tree = config_hash[:project_tree]
+ @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes],
+ excluded_attributes: config_hash[:excluded_attributes],
+ methods: config_hash[:methods])
+ end
+
+ # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
+ # for outputting a project in JSON format, including its relations and sub relations.
+ def project_tree
+ @attributes_finder.find_included(:project).merge(include: build_hash(@tree))
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
+ #
+ # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file
+ def build_hash(model_list)
+ model_list.map do |model_objects|
+ if model_objects.is_a?(Hash)
+ build_json_config_hash(model_objects)
+ else
+ @attributes_finder.find(model_objects)
+ end
+ end
+ end
+
+ # Called when the model is actually a hash containing other relations (more models)
+ # Returns the config in the right format for calling +to_json+
+ # +model_object_hash+ - A model relationship such as:
+ # {:merge_requests=>[:merge_request_diff, :notes]}
+ def build_json_config_hash(model_object_hash)
+ @json_config_hash = {}
+
+ model_object_hash.values.flatten.each do |model_object|
+ current_key = model_object_hash.keys.first
+
+ @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash }
+
+ handle_model_object(current_key, model_object)
+ process_sub_model(current_key, model_object) if model_object.is_a?(Hash)
+ end
+ @json_config_hash
+ end
+
+
+ # If the model is a hash, process the sub_models, which could also be hashes
+ # If there is a list, add to an existing array, otherwise use hash syntax
+ # +current_key+ main model that will be a key in the hash
+ # +model_object+ model or list of models to include in the hash
+ def process_sub_model(current_key, model_object)
+ sub_model_json = build_json_config_hash(model_object).dup
+ @json_config_hash.slice!(current_key)
+
+ if @json_config_hash[current_key] && @json_config_hash[current_key][:include]
+ @json_config_hash[current_key][:include] << sub_model_json
+ else
+ @json_config_hash[current_key] = { include: sub_model_json }
+ end
+ end
+
+ # Creates or adds to an existing hash an individual model or list
+ # +current_key+ main model that will be a key in the hash
+ # +model_object+ model or list of models to include in the hash
+ def handle_model_object(current_key, model_object)
+ if @json_config_hash[current_key]
+ add_model_value(current_key, model_object)
+ else
+ create_model_value(current_key, model_object)
+ end
+ end
+
+ # Constructs a new hash that will hold the configuration for that particular object
+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
+ # +current_key+ main model that will be a key in the hash
+ # +value+ existing model to be included in the hash
+ def create_model_value(current_key, value)
+ parsed_hash = { include: value }
+
+ @attributes_finder.parse(value) do |hash|
+ parsed_hash = { include: hash_or_merge(value, hash) }
+ end
+ @json_config_hash[current_key] = parsed_hash
+ end
+
+ # Adds new model configuration to an existing hash with key +current_key+
+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
+ # +current_key+ main model that will be a key in the hash
+ # +value+ existing model to be included in the hash
+ def add_model_value(current_key, value)
+ @attributes_finder.parse(value) { |hash| value = { value => hash } }
+ old_values = @json_config_hash[current_key][:include]
+ @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
+ end
+
+ # Construct a new hash or merge with an existing one a model configuration
+ # This is to fulfil +to_json+ requirements.
+ # +value+ existing model to be included in the hash
+ # +hash+ hash containing configuration generated mainly from +@attributes_finder+
+ def hash_or_merge(value, hash)
+ value.is_a?(Hash) ? value.merge(hash) : { value => hash }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
new file mode 100644
index 00000000000..b872780f20a
--- /dev/null
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -0,0 +1,128 @@
+module Gitlab
+ module ImportExport
+ class RelationFactory
+
+ OVERRIDES = { snippets: :project_snippets,
+ pipelines: 'Ci::Pipeline',
+ statuses: 'commit_status',
+ variables: 'Ci::Variable',
+ triggers: 'Ci::Trigger',
+ builds: 'Ci::Build',
+ hooks: 'ProjectHook' }.freeze
+
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
+
+ def self.create(*args)
+ new(*args).create
+ end
+
+ def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
+ @relation_name = OVERRIDES[relation_sym] || relation_sym
+ @relation_hash = relation_hash.except('id', 'noteable_id')
+ @members_mapper = members_mapper
+ @user = user
+ end
+
+ # Creates an object from an actual model with name "relation_sym" with params from
+ # the relation_hash, updating references with new object IDs, mapping users using
+ # the "members_mapper" object, also updating notes if required.
+ def create
+ set_note_author if @relation_name == :notes
+ update_user_references
+ update_project_references
+ reset_ci_tokens if @relation_name == 'Ci::Trigger'
+
+ generate_imported_object
+ end
+
+ private
+
+ def update_user_references
+ USER_REFERENCES.each do |reference|
+ if @relation_hash[reference]
+ @relation_hash[reference] = @members_mapper.map[@relation_hash[reference]]
+ end
+ end
+ end
+
+ # Sets the author for a note. If the user importing the project
+ # has admin access, an actual mapping with new project members
+ # will be used. Otherwise, a note stating the original author name
+ # is left.
+ def set_note_author
+ old_author_id = @relation_hash['author_id']
+
+ # Users with admin access can map users
+ @relation_hash['author_id'] = admin_user? ? @members_mapper.map[old_author_id] : @members_mapper.default_user_id
+
+ author = @relation_hash.delete('author')
+
+ update_note_for_missing_author(author['name']) if missing_author?(old_author_id)
+ end
+
+ def missing_author?(old_author_id)
+ !admin_user? || @members_mapper.missing_author_ids.include?(old_author_id)
+ end
+
+ def missing_author_note(updated_at, author_name)
+ timestamp = updated_at.split('.').first
+ "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*"
+ end
+
+ def generate_imported_object
+ if @relation_sym == 'commit_status' # call #trace= method after assigning the other attributes
+ trace = @relation_hash.delete('trace')
+ imported_object do |object|
+ object.trace = trace
+ object.commit_id = nil
+ end
+ else
+ imported_object
+ end
+ end
+
+ def update_project_references
+ project_id = @relation_hash.delete('project_id')
+
+ # project_id may not be part of the export, but we always need to populate it if required.
+ @relation_hash['project_id'] = project_id if relation_class.column_names.include?('project_id')
+ @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
+ @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
+ @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id']
+
+ # If source and target are the same, populate them with the new project ID.
+ if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] &&
+ @relation_hash['target_project_id'] == @relation_hash['source_project_id']
+ @relation_hash['source_project_id'] = project_id
+ end
+ end
+
+ def reset_ci_tokens
+ return unless Gitlab::ImportExport.reset_tokens?
+
+ # If we import/export a project to the same instance, tokens will have to be reset.
+ @relation_hash['token'] = nil
+ end
+
+ def relation_class
+ @relation_class ||= @relation_name.to_s.classify.constantize
+ end
+
+ def imported_object
+ imported_object = relation_class.new(@relation_hash)
+ yield(imported_object) if block_given?
+ imported_object.importing = true if imported_object.respond_to?(:importing)
+ imported_object
+ end
+
+ def update_note_for_missing_author(author_name)
+ @relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank?
+ @relation_hash['note'] += missing_author_note(@relation_hash['updated_at'], author_name)
+ end
+
+ def admin_user?
+ @user.is_admin?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
new file mode 100644
index 00000000000..546dae4d122
--- /dev/null
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module ImportExport
+ class RepoRestorer
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def initialize(project:, shared:, path_to_bundle:, wiki: false)
+ @project = project
+ @path_to_bundle = path_to_bundle
+ @shared = shared
+ @wiki = wiki
+ end
+
+ def restore
+ return wiki? unless File.exist?(@path_to_bundle)
+
+ FileUtils.mkdir_p(path_to_repo)
+
+ git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def repos_path
+ Gitlab.config.gitlab_shell.repos_path
+ end
+
+ def path_to_repo
+ @project.repository.path_to_repo
+ end
+
+ def wiki?
+ @wiki
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
new file mode 100644
index 00000000000..cce43fe994b
--- /dev/null
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module ImportExport
+ class RepoSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ attr_reader :full_path
+
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def save
+ return false if @project.empty_repo?
+
+ @full_path = File.join(@shared.export_path, ImportExport.project_bundle_filename)
+ bundle_to_disk
+ end
+
+ private
+
+ def bundle_to_disk
+ FileUtils.mkdir_p(@shared.export_path)
+ git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ def path_to_repo
+ @project.repository.path_to_repo
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
new file mode 100644
index 00000000000..f38229c6c59
--- /dev/null
+++ b/lib/gitlab/import_export/saver.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module ImportExport
+ class Saver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def self.save(*args)
+ new(*args).save
+ end
+
+ def initialize(shared:)
+ @shared = shared
+ end
+
+ def save
+ if compress_and_save
+ remove_export_path
+ Rails.logger.info("Saved project export #{archive_file}")
+ archive_file
+ else
+ false
+ end
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def compress_and_save
+ tar_czf(archive: archive_file, dir: @shared.export_path)
+ end
+
+ def remove_export_path
+ FileUtils.rm_rf(@shared.export_path)
+ end
+
+ def archive_file
+ @archive_file ||= File.join(@shared.export_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
new file mode 100644
index 00000000000..6aff05b886a
--- /dev/null
+++ b/lib/gitlab/import_export/shared.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module ImportExport
+ class Shared
+
+ attr_reader :errors, :opts
+
+ def initialize(opts)
+ @opts = opts
+ @errors = []
+ end
+
+ def export_path
+ @export_path ||= Gitlab::ImportExport.export_path(relative_path: opts[:relative_path])
+ end
+
+ def error(error)
+ error_out(error.message, caller[0].dup)
+ @errors << error.message
+ # Debug:
+ Rails.logger.error(error.backtrace)
+ end
+
+ private
+
+ def error_out(message, caller)
+ Rails.logger.error("Import/Export error raised on #{caller}: #{message}")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/uploads_restorer.rb b/lib/gitlab/import_export/uploads_restorer.rb
new file mode 100644
index 00000000000..df19354b76e
--- /dev/null
+++ b/lib/gitlab/import_export/uploads_restorer.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module ImportExport
+ class UploadsRestorer < UploadsSaver
+ def restore
+ return true unless File.directory?(uploads_export_path)
+
+ copy_files(uploads_export_path, uploads_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb
new file mode 100644
index 00000000000..7292e9d9712
--- /dev/null
+++ b/lib/gitlab/import_export/uploads_saver.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module ImportExport
+ class UploadsSaver
+
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def save
+ return true unless File.directory?(uploads_path)
+
+ copy_files(uploads_path, uploads_export_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def copy_files(source, destination)
+ FileUtils.mkdir_p(destination)
+ FileUtils.copy_entry(source, destination)
+ true
+ end
+
+ def uploads_export_path
+ File.join(@shared.export_path, 'uploads')
+ end
+
+ def uploads_path
+ File.join(Rails.root.join('public/uploads'), @project.path_with_namespace)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
new file mode 100644
index 00000000000..cf5c62c5e3c
--- /dev/null
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module ImportExport
+ class VersionChecker
+
+ def self.check!(*args)
+ new(*args).check!
+ end
+
+ def initialize(shared:)
+ @shared = shared
+ end
+
+ def check!
+ version = File.open(version_file, &:readline)
+ verify_version!(version)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def version_file
+ File.join(@shared.export_path, Gitlab::ImportExport.version_filename)
+ end
+
+ def verify_version!(version)
+ if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version)
+ raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ else
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb
new file mode 100644
index 00000000000..f7f73dc9343
--- /dev/null
+++ b/lib/gitlab/import_export/version_saver.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module ImportExport
+ class VersionSaver
+
+ def initialize(shared:)
+ @shared = shared
+ end
+
+ def save
+ FileUtils.mkdir_p(@shared.export_path)
+
+ File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def version_file
+ File.join(@shared.export_path, Gitlab::ImportExport.version_filename)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
new file mode 100644
index 00000000000..1eedae39f8a
--- /dev/null
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module ImportExport
+ class WikiRepoSaver < RepoSaver
+ def save
+ @wiki = ProjectWiki.new(@project)
+ return true unless wiki_repository_exists? # it's okay to have no Wiki
+ bundle_to_disk(File.join(@shared.export_path, project_filename))
+ end
+
+ def bundle_to_disk(full_path)
+ FileUtils.mkdir_p(@shared.export_path)
+ git_bundle(repo_path: path_to_repo, bundle_path: full_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def project_filename
+ "project.wiki.bundle"
+ end
+
+ def path_to_repo
+ @wiki.repository.path_to_repo
+ end
+
+ def wiki_repository_exists?
+ File.exist?(@wiki.repository.path_to_repo) && !@wiki.repository.empty?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index ccfdfbe73e8..948d43582cf 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -20,7 +20,8 @@ module Gitlab
'Gitorious.org' => 'gitorious',
'Google Code' => 'google_code',
'FogBugz' => 'fogbugz',
- 'Any repo by URL' => 'git',
+ 'Repo by URL' => 'git',
+ 'GitLab export' => 'gitlab_project'
}
end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index d81d26754fe..dcec7543c13 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -148,23 +148,8 @@ module Gitlab
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
- trans = Gitlab::Metrics::Instrumentation.transaction
-
- if trans
- start = Time.now
- cpu_start = Gitlab::Metrics::System.cpu_time
- retval = super
- duration = (Time.now - start) * 1000.0
-
- if duration >= Gitlab::Metrics.method_call_threshold
- cpu_duration = Gitlab::Metrics::System.cpu_time - cpu_start
-
- trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
- { duration: duration, cpu_duration: cpu_duration },
- method: #{label.inspect})
- end
-
- retval
+ if trans = Gitlab::Metrics::Instrumentation.transaction
+ trans.measure_method(#{label.inspect}) { super }
else
super
end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
new file mode 100644
index 00000000000..faf0d9b6318
--- /dev/null
+++ b/lib/gitlab/metrics/method_call.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Metrics
+ # Class for tracking timing information about method calls
+ class MethodCall
+ attr_reader :real_time, :cpu_time, :call_count
+
+ # name - The full name of the method (including namespace) such as
+ # `User#sign_in`.
+ #
+ # series - The series to use for storing the data.
+ def initialize(name, series)
+ @name = name
+ @series = series
+ @real_time = 0.0
+ @cpu_time = 0.0
+ @call_count = 0
+ end
+
+ # Measures the real and CPU execution time of the supplied block.
+ def measure
+ start_real = Time.now
+ start_cpu = System.cpu_time
+ retval = yield
+
+ @real_time += (Time.now - start_real) * 1000.0
+ @cpu_time += System.cpu_time.to_f - start_cpu
+ @call_count += 1
+
+ retval
+ end
+
+ # Returns a Metric instance of the current method call.
+ def to_metric
+ Metric.new(
+ @series,
+ {
+ duration: real_time,
+ cpu_duration: cpu_time,
+ call_count: call_count
+ },
+ method: @name
+ )
+ end
+
+ # Returns true if the total runtime of this method exceeds the method call
+ # threshold.
+ def above_threshold?
+ real_time >= Metrics.method_call_threshold
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 3fe27779d03..e61670f491c 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -35,7 +35,7 @@ module Gitlab
def transaction_from_env(env)
trans = Transaction.new
- trans.set(:request_uri, env['REQUEST_URI'])
+ trans.set(:request_uri, filtered_path(env))
trans.set(:request_method, env['REQUEST_METHOD'])
trans
@@ -54,6 +54,10 @@ module Gitlab
private
+ def filtered_path(env)
+ ActionDispatch::Request.new(env).filtered_path.presence || env['REQUEST_URI']
+ end
+
def endpoint_paths_cache
@endpoint_paths_cache ||= Hash.new do |hash, http_method|
hash[http_method] = Hash.new do |inner_hash, raw_path|
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 2578ddc49f4..4bc5081aa03 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,7 +4,7 @@ module Gitlab
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
- attr_reader :tags, :values
+ attr_reader :tags, :values, :methods
attr_accessor :action
@@ -16,6 +16,7 @@ module Gitlab
# plus method name.
def initialize(action = nil)
@metrics = []
+ @methods = {}
@started_at = nil
@finished_at = nil
@@ -51,9 +52,23 @@ module Gitlab
end
def add_metric(series, values, tags = {})
- prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+ @metrics << Metric.new("#{series_prefix}#{series}", values, tags)
+ end
+
+ # Measures the time it takes to execute a method.
+ #
+ # Multiple calls to the same method add up to the total runtime of the
+ # method.
+ #
+ # name - The full name of the method to measure (e.g. `User#sign_in`).
+ def measure_method(name, &block)
+ unless @methods[name]
+ series = "#{series_prefix}#{Instrumentation::SERIES}"
+
+ @methods[name] = MethodCall.new(name, series)
+ end
- @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ @methods[name].measure(&block)
end
def increment(name, value)
@@ -84,7 +99,13 @@ module Gitlab
end
def submit
- metrics = @metrics.map do |metric|
+ submit = @metrics.dup
+
+ @methods.each do |name, method|
+ submit << method.to_metric if method.above_threshold?
+ end
+
+ submit_hashes = submit.map do |metric|
hash = metric.to_hash
hash[:tags][:action] ||= @action if @action
@@ -92,12 +113,16 @@ module Gitlab
hash
end
- Metrics.submit_metrics(metrics)
+ Metrics.submit_metrics(submit_hashes)
end
def sidekiq?
Sidekiq.server?
end
+
+ def series_prefix
+ sidekiq? ? 'sidekiq_' : 'rails_'
+ end
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 388f84dbe0e..40e8299c36b 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -8,7 +8,7 @@ module Gitlab
class << self
def git_http_ok(repository, user)
{
- 'GL_ID' => Gitlab::ShellEnv.gl_id(user),
+ 'GL_ID' => Gitlab::GlId.gl_id(user),
'RepoPath' => repository.path_to_repo,
}
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 186239d3096..ff5b3916273 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -30,4 +30,75 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
end
+
+ describe "#authenticate_user_from_token!" do
+ describe "authenticating a user from a private token" do
+ controller(ApplicationController) do
+ def index
+ render text: "authenticated"
+ end
+ end
+
+ let(:user) { create(:user) }
+
+ context "when the 'private_token' param is populated with the private token" do
+ it "logs the user in" do
+ get :index, private_token: user.private_token
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+
+ context "when the 'PRIVATE-TOKEN' header is populated with the private token" do
+ it "logs the user in" do
+ @request.headers['PRIVATE-TOKEN'] = user.private_token
+ get :index
+ expect(response.status).to eq(200)
+ expect(response.body).to eq("authenticated")
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ @request.headers['PRIVATE-TOKEN'] = "token"
+ get :index, private_token: "token", authenticity_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq("authenticated")
+ end
+ end
+
+ describe "authenticating a user from a personal access token" do
+ controller(ApplicationController) do
+ def index
+ render text: 'authenticated'
+ end
+ end
+
+ let(:user) { create(:user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ context "when the 'personal_access_token' param is populated with the personal access token" do
+ it "logs the user in" do
+ get :index, private_token: personal_access_token.token
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
+ it "logs the user in" do
+ @request.headers["PRIVATE-TOKEN"] = personal_access_token.token
+ get :index
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('authenticated')
+ end
+ end
+
+ it "doesn't log the user in otherwise" do
+ get :index, private_token: "token"
+ expect(response.status).not_to eq(200)
+ expect(response.body).not_to eq('authenticated')
+ end
+ end
+ end
end
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
new file mode 100644
index 00000000000..da4c72bcb5b
--- /dev/null
+++ b/spec/factories/personal_access_tokens.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :personal_access_token do
+ user
+ token { SecureRandom.hex(50) }
+ name { FFaker::Product.brand }
+ revoked false
+ expires_at { 5.days.from_now }
+ end
+end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 7265cdac7a7..31633817d53 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -12,9 +12,11 @@ describe "Admin::Hooks", feature: true do
describe "GET /admin/hooks" do
it "should be ok" do
visit admin_root_path
- page.within ".sidebar-wrapper" do
+
+ page.within ".layout-nav" do
click_on "Hooks"
end
+
expect(current_path).to eq(admin_hooks_path)
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 65fe918e2e8..c3cb3379440 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -556,10 +556,10 @@ describe 'Issues', feature: true do
first('.ui-state-default').click
end
- expect(page).to have_no_content 'None'
+ expect(page).to have_no_content 'No due date'
click_link 'remove due date'
- expect(page).to have_content 'None'
+ expect(page).to have_content 'No due date'
end
end
end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
new file mode 100644
index 00000000000..a85930c7543
--- /dev/null
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'Profile > Personal Access Tokens', feature: true, js: true do
+ let(:user) { create(:user) }
+
+ def active_personal_access_tokens
+ find(".table.active-personal-access-tokens")
+ end
+
+ def inactive_personal_access_tokens
+ find(".table.inactive-personal-access-tokens")
+ end
+
+ def created_personal_access_token
+ find("#created-personal-access-token").value
+ end
+
+ def disallow_personal_access_token_saves!
+ allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false)
+ errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
+ allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
+ end
+
+ before do
+ login_as(user)
+ end
+
+ describe "token creation" do
+ it "allows creation of a token" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text("Never")
+ end
+
+ it "allows creation of a token with an expiry date" do
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ # Set date to 1st of next month
+ find_field("Expires at").trigger('focus')
+ find("a[title='Next']").click
+ click_on "1"
+
+ expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
+ expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
+ expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+ expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+ end
+
+ context "when creation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit profile_personal_access_tokens_path
+ fill_in "Name", with: FFaker::Product.brand
+
+ expect { click_on "Create Personal Access Token" }.not_to change { PersonalAccessToken.count }
+ expect(page).to have_content("Name cannot be nil")
+ end
+ end
+ end
+
+ describe "inactive tokens" do
+ let!(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "allows revocation of an active token" do
+ visit profile_personal_access_tokens_path
+ click_on "Revoke"
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ it "moves expired tokens to the 'inactive' section" do
+ personal_access_token.update(expires_at: 5.days.ago)
+ visit profile_personal_access_tokens_path
+
+ expect(inactive_personal_access_tokens).to have_text(personal_access_token.name)
+ end
+
+ context "when revocation fails" do
+ it "displays an error message" do
+ disallow_personal_access_token_saves!
+ visit profile_personal_access_tokens_path
+
+ expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
+ expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+ expect(page).to have_content("Could not revoke")
+ end
+ end
+ 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 ecc818eb1e1..e1e105e6bbe 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
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'project owner creates a license file', feature: true, js: true do
- include Select2Helper
+ include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:project) }
@@ -21,7 +21,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
@@ -44,7 +44,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
@@ -58,4 +58,12 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(page).to have_content('The MIT License (MIT)')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
+
+ def select_template(template)
+ page.within('.js-license-selector-wrap') do
+ click_button 'Choose a License template'
+ click_link template
+ wait_for_ajax
+ end
+ end
end
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 34eda29c285..67aac25e427 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
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
- include Select2Helper
+ include WaitForAjax
let(:project_master) { create(:user) }
let(:project) { create(:empty_project) }
@@ -20,7 +20,7 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(find('#file_name').value).to eq('LICENSE')
expect(page).to have_selector('.license-selector')
- select2('mit', from: '#license_type')
+ select_template('MIT License')
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
@@ -36,4 +36,12 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(page).to have_content('The MIT License (MIT)')
expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
+
+ def select_template(template)
+ page.within('.js-license-selector-wrap') do
+ click_button 'Choose a License template'
+ click_link template
+ wait_for_ajax
+ end
+ end
end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
new file mode 100644
index 00000000000..c5fb0fc783b
--- /dev/null
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -0,0 +1,49 @@
+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(: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 }
+
+ 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
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with:'test-project-path', visible: true
+ click_link 'GitLab export'
+
+ expect(page).to have_content('GitLab project export')
+ expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+
+ attach_file('file', file)
+
+ 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.repo_exists?).to be true
+ expect(wiki_exists?).to be true
+ expect(project.import_status).to eq('finished')
+ end
+
+ def wiki_exists?
+ wiki = ProjectWiki.new(project)
+ File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty?
+ end
+end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
new file mode 100644
index 00000000000..1fd04416d95
--- /dev/null
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 8550d279d09..6a39c302f55 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -77,6 +77,7 @@ feature 'Prioritize labels', feature: true do
end
visit current_url
+ wait_for_ajax
page.within('.prioritized-labels') do
expect(first('li')).to have_content('wontfix')
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
new file mode 100644
index 00000000000..728c0e16361
--- /dev/null
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group member cannot leave group project', feature: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ background do
+ group.add_developer(user)
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user does not see a "Leave project" link' do
+ expect(page).not_to have_content 'Leave Project'
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 029a11ea43c..b9e63a7152c 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -47,4 +47,83 @@ describe "Search", feature: true do
expect(page).to have_link(snippet.title)
end
end
+
+
+ describe 'Right header search field', feature: true do
+
+ describe 'Search in project page' do
+ before do
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'top right search form is present' do
+ expect(page).to have_selector('#search')
+ end
+
+ it 'top right search form contains location badge' do
+ expect(page).to have_selector('.has-location-badge')
+ end
+
+ context 'clicking the search field', js: true do
+ it 'should show category search dropdown' do
+ page.find('#search').click
+
+ expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+ end
+
+ context 'click the links in the category search dropdown', js: true do
+
+ before do
+ page.find('#search').click
+ end
+
+ it 'should take user to her issues page when issues assigned is clicked' do
+ find('.dropdown-menu').click_link 'Issues assigned to me'
+ sleep 2
+
+ expect(page).to have_selector('.issues-holder')
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should take user to her issues page when issues authored is clicked' do
+ find('.dropdown-menu').click_link "Issues I've created"
+ sleep 2
+
+ expect(page).to have_selector('.issues-holder')
+ expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should take user to her MR page when MR assigned is clicked' do
+ find('.dropdown-menu').click_link 'Merge requests assigned to me'
+ sleep 2
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+
+ it 'should take user to her MR page when MR authored is clicked' do
+ find('.dropdown-menu').click_link "Merge requests I've created"
+ sleep 2
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ end
+ end
+
+ context 'entering text into the search field', js: true do
+ before do
+ page.within '.search-input-wrap' do
+ fill_in "search", with: project.name[0..3]
+ end
+ end
+
+ it 'should not display the category search dropdown' do
+ expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+ end
+ end
+ end
+
+
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 639b28d49ee..1bd354815e4 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -49,6 +49,13 @@ describe NotesFinder do
user = create(:user)
expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
end
+
+ it 'raises an error for project members with guest role' do
+ user = create(:user)
+ project.team << [user, :guest]
+
+ expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 0b1a76156e0..7998209b7b0 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -9,22 +9,6 @@ describe MembersHelper do
it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
end
- describe '#can_see_member_roles?' do
- let(:project) { create(:empty_project) }
- let(:group) { create(:group) }
- let(:user) { build(:user) }
- let(:admin) { build(:user, :admin) }
- let(:project_member) { create(:project_member, project: project) }
- let(:group_member) { create(:group_member, group: group) }
-
- it { expect(can_see_member_roles?(source: project, user: nil)).to be_falsy }
- it { expect(can_see_member_roles?(source: group, user: nil)).to be_falsy }
- it { expect(can_see_member_roles?(source: project, user: admin)).to be_truthy }
- it { expect(can_see_member_roles?(source: group, user: admin)).to be_truthy }
- it { expect(can_see_member_roles?(source: project, user: project_member.user)).to be_truthy }
- it { expect(can_see_member_roles?(source: group, user: group_member.user)).to be_truthy }
- end
-
describe '#remove_member_message' do
let(:requester) { build(:user) }
let(:project) { create(:project) }
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index a3336c87173..903224589dd 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -33,9 +33,9 @@ describe MergeRequestsHelper do
let(:project) { create(:project) }
let(:issues) do
[
- JiraIssue.new('JIRA-123', project),
- JiraIssue.new('JIRA-456', project),
- JiraIssue.new('FOOBAR-7890', project)
+ ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('JIRA-456', project),
+ ExternalIssue.new('FOOBAR-7890', project)
]
end
diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml
new file mode 100644
index 00000000000..7785120da5b
--- /dev/null
+++ b/spec/javascripts/fixtures/search_autocomplete.html.haml
@@ -0,0 +1,10 @@
+.search.search-form.has-location-badge
+ %form.navbar-form
+ .search-input-container
+ %div.location-badge
+ This project
+ .search-input-wrap
+ .dropdown
+ %input#search.search-input.dropdown-menu-toggle
+ .dropdown-menu.dropdown-select
+ .dropdown-content
diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee
index 22ebc7039d1..3cb67d51c85 100644
--- a/spec/javascripts/merge_request_spec.js.coffee
+++ b/spec/javascripts/merge_request_spec.js.coffee
@@ -6,7 +6,7 @@ describe 'MergeRequest', ->
beforeEach ->
fixture.load('merge_requests_show.html')
- @merge = new MergeRequest({})
+ @merge = new MergeRequest()
it 'modifies the Markdown field', ->
spyOn(jQuery, 'ajax').and.stub()
diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee
index dd160e821b3..3a3c8d63e82 100644
--- a/spec/javascripts/notes_spec.js.coffee
+++ b/spec/javascripts/notes_spec.js.coffee
@@ -1,7 +1,7 @@
#= require notes
#= require gl_form
-window.gon = {}
+window.gon or= {}
window.disableButtonIfEmptyField = -> null
describe 'Notes', ->
diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee
index 1cf34d4d2d3..9be29097f4c 100644
--- a/spec/javascripts/project_title_spec.js.coffee
+++ b/spec/javascripts/project_title_spec.js.coffee
@@ -6,7 +6,7 @@
#= require project_select
#= require project
-window.gon = {}
+window.gon or= {}
window.gon.api_version = 'v3'
describe 'Project Title', ->
diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee
new file mode 100644
index 00000000000..e77177783a7
--- /dev/null
+++ b/spec/javascripts/search_autocomplete_spec.js.coffee
@@ -0,0 +1,149 @@
+#= require gl_dropdown
+#= require search_autocomplete
+#= require jquery
+#= require lib/common_utils
+#= require lib/type_utility
+#= require fuzzaldrin-plus
+
+
+widget = null
+userId = 1
+window.gon or= {}
+window.gon.current_user_id = userId
+
+dashboardIssuesPath = '/dashboard/issues'
+dashboardMRsPath = '/dashboard/merge_requests'
+projectIssuesPath = '/gitlab-org/gitlab-ce/issues'
+projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'
+groupIssuesPath = '/groups/gitlab-org/issues'
+groupMRsPath = '/groups/gitlab-org/merge_requests'
+projectName = 'GitLab Community Edition'
+groupName = 'Gitlab Org'
+
+
+# Add required attributes to body before starting the test.
+# section would be dashboard|group|project
+addBodyAttributes = (section = 'dashboard') ->
+
+ $body = $ 'body'
+
+ $body.removeAttr 'data-page'
+ $body.removeAttr 'data-project'
+ $body.removeAttr 'data-group'
+
+ switch section
+ when 'dashboard'
+ $body.data 'page', 'root:index'
+ when 'group'
+ $body.data 'page', 'groups:show'
+ $body.data 'group', 'gitlab-org'
+ when 'project'
+ $body.data 'page', 'projects:show'
+ $body.data 'project', 'gitlab-ce'
+
+
+# Mock `gl` object in window for dashboard specific page. App code will need it.
+mockDashboardOptions = ->
+
+ window.gl or= {}
+ window.gl.dashboardOptions =
+ issuesPath: dashboardIssuesPath
+ mrPath : dashboardMRsPath
+
+
+# Mock `gl` object in window for project specific page. App code will need it.
+mockProjectOptions = ->
+
+ window.gl or= {}
+ window.gl.projectOptions =
+ 'gitlab-ce' :
+ issuesPath : projectIssuesPath
+ mrPath : projectMRsPath
+ projectName : projectName
+
+
+mockGroupOptions = ->
+
+ window.gl or= {}
+ window.gl.groupOptions =
+ 'gitlab-org' :
+ issuesPath : groupIssuesPath
+ mrPath : groupMRsPath
+ projectName : groupName
+
+
+assertLinks = (list, issuesPath, mrsPath) ->
+
+ issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}"
+ issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}"
+ mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}"
+ mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}"
+
+ a1 = "a[href='#{issuesAssignedToMeLink}']"
+ a2 = "a[href='#{issuesIHaveCreatedLink}']"
+ a3 = "a[href='#{mrsAssignedToMeLink}']"
+ a4 = "a[href='#{mrsIHaveCreatedLink}']"
+
+ expect(list.find(a1).length).toBe 1
+ expect(list.find(a1).text()).toBe ' Issues assigned to me '
+
+ expect(list.find(a2).length).toBe 1
+ expect(list.find(a2).text()).toBe " Issues I've created "
+
+ expect(list.find(a3).length).toBe 1
+ expect(list.find(a3).text()).toBe ' Merge requests assigned to me '
+
+ expect(list.find(a4).length).toBe 1
+ expect(list.find(a4).text()).toBe " Merge requests I've created "
+
+
+describe 'Search autocomplete dropdown', ->
+
+ fixture.preload 'search_autocomplete.html'
+
+ beforeEach ->
+
+ fixture.load 'search_autocomplete.html'
+ widget = new SearchAutocomplete
+
+
+ it 'should show Dashboard specific dropdown menu', ->
+
+ addBodyAttributes()
+ mockDashboardOptions()
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ assertLinks list, dashboardIssuesPath, dashboardMRsPath
+
+
+ it 'should show Group specific dropdown menu', ->
+
+ addBodyAttributes 'group'
+ mockGroupOptions()
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ assertLinks list, groupIssuesPath, groupMRsPath
+
+
+ it 'should show Project specific dropdown menu', ->
+
+ addBodyAttributes 'project'
+ mockProjectOptions()
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ assertLinks list, projectIssuesPath, projectMRsPath
+
+
+ it 'should not show category related menu if there is text in the input', ->
+
+ addBodyAttributes 'project'
+ mockProjectOptions()
+ widget.searchInput.val 'help'
+ widget.searchInput.focus()
+
+ list = widget.wrap.find('.dropdown-menu').find 'ul'
+ link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']"
+ expect(list.find(link).length).toBe 0
diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
new file mode 100644
index 00000000000..0c55d8e19da
--- /dev/null
+++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Banzai::Filter::AbstractReferenceFilter do
+ let(:project) { create(:empty_project) }
+
+ describe '#references_per_project' do
+ it 'returns a Hash containing references grouped per project paths' do
+ doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2")
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:object_class).twice.and_return(Issue)
+ expect(filter).to receive(:object_sym).twice.and_return(:issue)
+
+ refs = filter.references_per_project
+
+ expect(refs).to be_an_instance_of(Hash)
+ expect(refs[project.to_reference]).to eq(Set.new(%w[1 2]))
+ end
+ end
+
+ describe '#projects_per_reference' do
+ it 'returns a Hash containing projects grouped per project paths' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter).to receive(:references_per_project).
+ and_return({ project.path_with_namespace => Set.new(%w[1]) })
+
+ expect(filter.projects_per_reference).
+ to eq({ project.path_with_namespace => project })
+ end
+ end
+
+ describe '#find_projects_for_paths' do
+ it 'returns a list of Projects for a list of paths' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter.find_projects_for_paths([project.path_with_namespace])).
+ to eq([project])
+ end
+ end
+
+ describe '#current_project_path' do
+ it 'returns the path of the current project' do
+ doc = Nokogiri::HTML.fragment('')
+ filter = described_class.new(doc, project: project)
+
+ expect(filter.current_project_path).to eq(project.path_with_namespace)
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb
index f4c5c621bd0..695a5bc6fd4 100644
--- a/spec/lib/banzai/filter/external_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -19,19 +19,31 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
- it 'adds rel="nofollow" to external links' do
- act = %q(<a href="https://google.com/">Google</a>)
- doc = filter(act)
-
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'nofollow'
+ context 'for root links on document' do
+ let(:doc) { filter %q(<a href="https://google.com/">Google</a>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
+
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
end
- it 'adds rel="noreferrer" to external links' do
- act = %q(<a href="https://google.com/">Google</a>)
- doc = filter(act)
+ context 'for nested links on document' do
+ let(:doc) { filter %q(<p><a href="https://google.com/">Google</a></p>) }
+
+ it 'adds rel="nofollow" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'nofollow'
+ end
- expect(doc.at_css('a')).to have_attribute('rel')
- expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ it 'adds rel="noreferrer" to external links' do
+ expect(doc.at_css('a')).to have_attribute('rel')
+ expect(doc.at_css('a')['rel']).to include 'noreferrer'
+ end
end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 8e6a264970d..25f0bc2092f 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -25,7 +25,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
- expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project, issue.iid).
+ and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
@@ -107,8 +109,9 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
let(:reference) { issue.to_reference(project) }
it 'ignores valid references when cross-reference project uses external tracker' do
- expect_any_instance_of(Project).to receive(:get_issue).
- with(issue.iid).and_return(nil)
+ expect_any_instance_of(described_class).to receive(:find_object).
+ with(project2, issue.iid).
+ and_return(nil)
exp = act = "Issue #{reference}"
expect(reference_filter(act).to_html).to eq exp
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index b83be54746c..273d2ed709a 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -23,6 +23,14 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do
%(<a href="#{path}">#{path}</a>)
end
+ def nested_image(path)
+ %(<div><img src="#{path}" /></div>)
+ end
+
+ def nested_link(path)
+ %(<div><a href="#{path}">#{path}</a></div>)
+ end
+
let(:project) { create(:project) }
shared_examples :preserve_unchanged do
@@ -47,11 +55,19 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do
doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
expect(doc.at_css('a')['href']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = filter(nested_link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('a')['href']).
+ to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
end
it 'rebuilds relative URL for an image' do
- doc = filter(link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
- expect(doc.at_css('a')['href']).
+ doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('img')['src']).
+ to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = filter(nested_image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('img')['src']).
to eq "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 143e2e6d238..d562d8b25ea 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -157,6 +157,35 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
end
+
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ shared_examples 'raises an error' do
+ it do
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is integer' do
+ let(:only) { 1 }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is an array of integers' do
+ let(:only) { [1, 1] }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is invalid regex' do
+ let(:only) { ["/*invalid/"] }
+
+ it_behaves_like 'raises an error'
+ end
+ end
end
describe :except do
@@ -284,16 +313,44 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
end
- end
+ context 'for invalid value' do
+ let(:config) { { rspec: { script: "rspec", except: except } } }
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+
+ shared_examples 'raises an error' do
+ it do
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps')
+ end
+ end
+
+ context 'when it is integer' do
+ let(:except) { 1 }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is an array of integers' do
+ let(:except) { [1, 1] }
+
+ it_behaves_like 'raises an error'
+ end
+
+ context 'when it is invalid regex' do
+ let(:except) { ["/*invalid/"] }
+
+ it_behaves_like 'raises an error'
+ end
+ end
+ end
end
-
+
describe "Scripts handling" do
let(:config_data) { YAML.dump(config) }
let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
-
+
subject { config_processor.builds_for_stage_and_ref("test", "master").first }
-
+
describe "before_script" do
context "in global context" do
let(:config) do
@@ -302,12 +359,12 @@ module Ci
test: { script: ["script"] }
}
end
-
+
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("global script\nscript")
end
end
-
+
context "overwritten in local context" do
let(:config) do
{
@@ -465,19 +522,41 @@ module Ci
end
context 'when syntax is incorrect' do
- it 'raises error' do
- variables = [:KEY1, 'value1', :KEY2, 'value2']
-
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: variables,
- script: 'rspec' }
- })
+ context 'when variables defined but invalid' do
+ it 'raises error' do
+ variables = [:KEY1, 'value1', :KEY2, 'value2']
+
+ config = YAML.dump(
+ { before_script: ['pwd'],
+ rspec: {
+ variables: variables,
+ script: 'rspec' }
+ })
+
+ expect { GitlabCiYamlProcessor.new(config, path) }
+ .to raise_error(GitlabCiYamlProcessor::ValidationError,
+ /job: variables should be a map/)
+ end
+ end
- expect { GitlabCiYamlProcessor.new(config, path) }
- .to raise_error(GitlabCiYamlProcessor::ValidationError,
- /job: variables should be a map/)
+ context 'when variables key defined but value not specified' do
+ it 'returns empty array' do
+ config = YAML.dump(
+ { before_script: ['pwd'],
+ rspec: {
+ variables: nil,
+ script: 'rspec' }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ ##
+ # TODO, in next version of CI configuration processor this
+ # should be invalid configuration, see #18775 and #15060
+ #
+ expect(config_processor.job_variables(:rspec))
+ .to be_an_instance_of(Array).and be_empty
+ end
end
end
end
diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb
index 279709521c9..c364e759108 100644
--- a/spec/lib/container_registry/repository_spec.rb
+++ b/spec/lib/container_registry/repository_spec.rb
@@ -21,7 +21,7 @@ describe ContainerRegistry::Repository do
to_return(
status: 200,
body: JSON.dump(tags: ['test']),
- headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
+ headers: { 'Content-Type' => 'application/json' })
end
context '#manifest' do
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 1ec539066a7..9096ad101b0 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -71,6 +71,18 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(Project.where(archived: true).count).to eq(5)
end
+
+ context 'when a block is supplied' do
+ it 'yields an Arel table and query object to the supplied block' do
+ first_id = Project.first.id
+
+ model.update_column_in_batches(:projects, :archived, true) do |t, query|
+ query.where(t[:id].eq(first_id))
+ end
+
+ expect(Project.where(archived: true).count).to eq(1)
+ end
+ end
end
describe '#add_column_with_default' do
@@ -78,7 +90,7 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
before do
expect(model).to receive(:transaction_open?).and_return(false)
- expect(model).to receive(:transaction).twice.and_yield
+ expect(model).to receive(:transaction).and_yield
expect(model).to receive(:add_column).
with(:projects, :foo, :integer, default: nil)
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
new file mode 100644
index 00000000000..f135a285dfb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::MembersMapper, services: true do
+ describe 'map members' do
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, name: 'searchable_project') }
+ let(:user2) { create(:user) }
+ let(:exported_user_id) { 99 }
+ let(:exported_members) do
+ [{
+ "id" => 2,
+ "access_level" => 40,
+ "source_id" => 14,
+ "source_type" => "Project",
+ "user_id" => 19,
+ "notification_level" => 3,
+ "created_at" => "2016-03-11T10:21:44.822Z",
+ "updated_at" => "2016-03-11T10:21:44.822Z",
+ "created_by_id" => nil,
+ "invite_email" => nil,
+ "invite_token" => nil,
+ "invite_accepted_at" => nil,
+ "user" =>
+ {
+ "id" => exported_user_id,
+ "email" => user2.email,
+ "username" => user2.username
+ }
+ }]
+ end
+
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user, project: project)
+ end
+
+ it 'maps a project member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+
+ it 'defaults to importer project member if it does not exist' do
+ expect(members_mapper.map[-1]).to eq(user.id)
+ end
+
+ it 'updates missing author IDs on missing project member' do
+ members_mapper.map[-1]
+
+ expect(members_mapper.missing_author_ids.first).to eq(-1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
new file mode 100644
index 00000000000..400d44ac162
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.json
@@ -0,0 +1,5341 @@
+{
+ "name": "Gitlab Test",
+ "path": "gitlab-test",
+ "description": "Aut saepe in eos dolorem aliquam hic.",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "visibility_level": 20,
+ "archived": false,
+ "issues": [
+ {
+ "id": 40,
+ "title": "Voluptatem modi rerum ipsum vero voluptas repudiandae veniam quibusdam.",
+ "assignee_id": 1,
+ "author_id": 4,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:28.411Z",
+ "updated_at": "2016-04-12T13:08:26.029Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Aut minima non sit qui nulla rerum laborum.",
+ "milestone_id": 10,
+ "state": "opened",
+ "iid": 10,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 1357,
+ "note": "test",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-04-12T13:08:26.006Z",
+ "updated_at": "2016-04-12T13:08:26.006Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 338,
+ "note": "Fugit in aliquid voluptas dolor.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:19:59.213Z",
+ "updated_at": "2016-03-22T15:19:59.213Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 337,
+ "note": "Occaecati consequatur facilis doloribus omnis hic placeat nihil.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:19:59.186Z",
+ "updated_at": "2016-03-22T15:19:59.186Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 336,
+ "note": "Nostrum et et est repudiandae non dolores voluptatem.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:19:59.156Z",
+ "updated_at": "2016-03-22T15:19:59.156Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 335,
+ "note": "Nihil et aut dolorum aut sit maxime.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:19:59.130Z",
+ "updated_at": "2016-03-22T15:19:59.130Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 334,
+ "note": "Non blanditiis voluptatem sit earum accusantium distinctio voluptas officiis.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:19:59.101Z",
+ "updated_at": "2016-03-22T15:19:59.101Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 333,
+ "note": "Nesciunt non dolorem similique nam ipsa et.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:19:59.075Z",
+ "updated_at": "2016-03-22T15:19:59.075Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 332,
+ "note": "Sed aut fugit et officiis dolor.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:19:59.047Z",
+ "updated_at": "2016-03-22T15:19:59.047Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 331,
+ "note": "Officiis iste eum recusandae suscipit consequatur consequatur.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:19:59.015Z",
+ "updated_at": "2016-03-22T15:19:59.015Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 40,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 39,
+ "title": "Sit ut adipisci sint temporibus velit quis.",
+ "assignee_id": 1,
+ "author_id": 12,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:28.278Z",
+ "updated_at": "2016-03-22T15:19:59.473Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Ab sint nostrum aliquam laudantium magni recusandae qui.",
+ "milestone_id": 10,
+ "state": "closed",
+ "iid": 9,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 346,
+ "note": "Natus rerum qui dolorem dolorum voluptas.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:19:59.469Z",
+ "updated_at": "2016-03-22T15:19:59.469Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 345,
+ "note": "Voluptatibus et qui quis id sed necessitatibus quos.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:19:59.438Z",
+ "updated_at": "2016-03-22T15:19:59.438Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 344,
+ "note": "Aperiam possimus ipsam quibusdam in.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:19:59.410Z",
+ "updated_at": "2016-03-22T15:19:59.410Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 343,
+ "note": "Ad vel hic molestiae tempora.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:19:59.379Z",
+ "updated_at": "2016-03-22T15:19:59.379Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 342,
+ "note": "Vel magnam sed quidem aut molestiae facilis alias.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:19:59.348Z",
+ "updated_at": "2016-03-22T15:19:59.348Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 341,
+ "note": "Veritatis dolorum aut qui quod.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:19:59.319Z",
+ "updated_at": "2016-03-22T15:19:59.319Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 340,
+ "note": "Illum at cumque dolorum et quia.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:19:59.289Z",
+ "updated_at": "2016-03-22T15:19:59.289Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 339,
+ "note": "Fugiat et error molestiae cumque quos aperiam.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:19:59.255Z",
+ "updated_at": "2016-03-22T15:19:59.255Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 39,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 38,
+ "title": "Quod quo est quis vel natus nulla eos reiciendis.",
+ "assignee_id": 12,
+ "author_id": 3,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:28.137Z",
+ "updated_at": "2016-03-22T15:19:59.712Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Fugit dolor accusantium suscipit facere voluptate.",
+ "milestone_id": 10,
+ "state": "opened",
+ "iid": 8,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 354,
+ "note": "Id commodi natus vel corrupti ea placeat cum nihil.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:19:59.708Z",
+ "updated_at": "2016-03-22T15:19:59.708Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 353,
+ "note": "Quia hic sed ratione eos voluptate dolor occaecati dolorem.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:19:59.680Z",
+ "updated_at": "2016-03-22T15:19:59.680Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 352,
+ "note": "Commodi sint voluptatem est aut.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:19:59.650Z",
+ "updated_at": "2016-03-22T15:19:59.650Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 351,
+ "note": "Et quibusdam voluptatibus dolores aut quam architecto optio.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:19:59.622Z",
+ "updated_at": "2016-03-22T15:19:59.622Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 350,
+ "note": "Fugit natus explicabo sed pariatur et quasi autem.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:19:59.590Z",
+ "updated_at": "2016-03-22T15:19:59.590Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 349,
+ "note": "Corporis commodi eos quia optio sunt corrupti.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:19:59.562Z",
+ "updated_at": "2016-03-22T15:19:59.562Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 348,
+ "note": "Occaecati nostrum hic dolor tenetur aliquid maxime animi.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:19:59.536Z",
+ "updated_at": "2016-03-22T15:19:59.536Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 347,
+ "note": "Inventore ullam sed repellendus laudantium itaque et quia.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:19:59.506Z",
+ "updated_at": "2016-03-22T15:19:59.506Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 38,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 37,
+ "title": "Animi suscipit quia ut hic asperiores perferendis nisi ut.",
+ "assignee_id": 22,
+ "author_id": 10,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.994Z",
+ "updated_at": "2016-03-22T15:19:59.972Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Non quibusdam in maxime earum eveniet itaque culpa.",
+ "milestone_id": 11,
+ "state": "closed",
+ "iid": 7,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 362,
+ "note": "Quia qui quis molestiae in praesentium.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:19:59.966Z",
+ "updated_at": "2016-03-22T15:19:59.966Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 361,
+ "note": "Maxime sed eius qui consequatur beatae.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:19:59.924Z",
+ "updated_at": "2016-03-22T15:19:59.924Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 360,
+ "note": "Voluptatum quasi corrupti eveniet sed ut quis quibusdam.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:19:59.897Z",
+ "updated_at": "2016-03-22T15:19:59.897Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 359,
+ "note": "Molestias quia eius ipsum non.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:19:59.866Z",
+ "updated_at": "2016-03-22T15:19:59.866Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 358,
+ "note": "Aut non est accusantium aliquam.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:19:59.834Z",
+ "updated_at": "2016-03-22T15:19:59.834Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 357,
+ "note": "Aspernatur voluptas id voluptas vel cum ipsam.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:19:59.805Z",
+ "updated_at": "2016-03-22T15:19:59.805Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 356,
+ "note": "Harum dignissimos provident tempora sit numquam est qui.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:19:59.773Z",
+ "updated_at": "2016-03-22T15:19:59.773Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 355,
+ "note": "Sint dignissimos molestiae recusandae delectus.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:19:59.746Z",
+ "updated_at": "2016-03-22T15:19:59.746Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 37,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 36,
+ "title": "Quia dolores commodi eligendi ut nemo totam.",
+ "assignee_id": 3,
+ "author_id": 4,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.814Z",
+ "updated_at": "2016-03-22T15:20:00.371Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Molestiae veniam laudantium autem et natus.",
+ "milestone_id": 11,
+ "state": "opened",
+ "iid": 6,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 370,
+ "note": "Occaecati temporibus tempore harum vero incidunt veniam iste.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:00.365Z",
+ "updated_at": "2016-03-22T15:20:00.365Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 369,
+ "note": "Modi architecto officiis quia iste voluptas libero nihil quo.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:00.331Z",
+ "updated_at": "2016-03-22T15:20:00.331Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 368,
+ "note": "Eaque est tenetur ex est molestiae nobis.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:00.296Z",
+ "updated_at": "2016-03-22T15:20:00.296Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 367,
+ "note": "Odit enim ut a quo qui.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:00.261Z",
+ "updated_at": "2016-03-22T15:20:00.261Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 366,
+ "note": "Omnis unde cum officiis est.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:00.223Z",
+ "updated_at": "2016-03-22T15:20:00.223Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 365,
+ "note": "Ab consequuntur aliquam illo voluptatum.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:00.178Z",
+ "updated_at": "2016-03-22T15:20:00.178Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 364,
+ "note": "Molestiae dolorem est eos dolores aut.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:00.127Z",
+ "updated_at": "2016-03-22T15:20:00.127Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 363,
+ "note": "Nemo velit nam quod veniam.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:00.083Z",
+ "updated_at": "2016-03-22T15:20:00.083Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 36,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 35,
+ "title": "Rerum tenetur harum molestiae quam aut praesentium quaerat doloremque.",
+ "assignee_id": 4,
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.660Z",
+ "updated_at": "2016-03-22T15:20:00.665Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Omnis et voluptatibus expedita qui et explicabo rem ut.",
+ "milestone_id": 11,
+ "state": "opened",
+ "iid": 5,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 378,
+ "note": "Molestiae atque exercitationem culpa harum nemo.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:00.660Z",
+ "updated_at": "2016-03-22T15:20:00.660Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 377,
+ "note": "Porro sed nobis neque amet velit velit.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:00.625Z",
+ "updated_at": "2016-03-22T15:20:00.625Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 376,
+ "note": "Dicta officiis doloremque voluptatum qui omnis.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:00.589Z",
+ "updated_at": "2016-03-22T15:20:00.589Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 375,
+ "note": "Incidunt rerum omnis cum laudantium aut impedit.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:00.553Z",
+ "updated_at": "2016-03-22T15:20:00.553Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 374,
+ "note": "Et suscipit omnis dolorum officia vero.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:00.517Z",
+ "updated_at": "2016-03-22T15:20:00.517Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 373,
+ "note": "Doloremque adipisci et cumque inventore beatae consectetur.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:00.485Z",
+ "updated_at": "2016-03-22T15:20:00.485Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 372,
+ "note": "Dolores sapiente ea dolorum et quae adipisci id.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:00.455Z",
+ "updated_at": "2016-03-22T15:20:00.455Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 371,
+ "note": "Accusantium repellat tenetur natus dicta ullam saepe facere.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:00.420Z",
+ "updated_at": "2016-03-22T15:20:00.420Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 35,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 34,
+ "title": "Enim occaecati aut sed quia mollitia eligendi atque dolores voluptatem.",
+ "assignee_id": 24,
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.506Z",
+ "updated_at": "2016-03-22T15:20:00.961Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Voluptatem totam magnam fugit assumenda consequatur illo qui.",
+ "milestone_id": 10,
+ "state": "opened",
+ "iid": 4,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 379,
+ "note": "Praesentium odio quia fugit consequuntur repudiandae ducimus.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:00.717Z",
+ "updated_at": "2016-03-22T15:20:00.717Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ },
+ {
+ "id": 380,
+ "note": "Dolores aut dolorem quia soluta incidunt commodi quia.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:00.754Z",
+ "updated_at": "2016-03-22T15:20:00.754Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 381,
+ "note": "Enim et velit iure ad.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:00.787Z",
+ "updated_at": "2016-03-22T15:20:00.787Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 382,
+ "note": "Impedit nobis quis laudantium ad assumenda.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:00.822Z",
+ "updated_at": "2016-03-22T15:20:00.822Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 383,
+ "note": "Facere sed numquam quos quas.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:00.855Z",
+ "updated_at": "2016-03-22T15:20:00.855Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 384,
+ "note": "Ex voluptatem sit provident error.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:00.889Z",
+ "updated_at": "2016-03-22T15:20:00.889Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 385,
+ "note": "Soluta laboriosam recusandae est cupiditate.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:00.925Z",
+ "updated_at": "2016-03-22T15:20:00.925Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 386,
+ "note": "Similique dolorem rerum iusto animi perferendis aut inventore.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:00.957Z",
+ "updated_at": "2016-03-22T15:20:00.957Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 34,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ }
+ ]
+ },
+ {
+ "id": 33,
+ "title": "Rem fugiat fugit occaecati quibusdam enim consectetur numquam.",
+ "assignee_id": 22,
+ "author_id": 22,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.364Z",
+ "updated_at": "2016-03-22T15:20:01.227Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Provident nulla architecto neque beatae fuga alias repudiandae.",
+ "milestone_id": 10,
+ "state": "closed",
+ "iid": 3,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 394,
+ "note": "Suscipit numquam voluptatibus ipsam libero dolorum dolore totam.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:01.223Z",
+ "updated_at": "2016-03-22T15:20:01.223Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 393,
+ "note": "Et et sed sit sint.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:01.194Z",
+ "updated_at": "2016-03-22T15:20:01.194Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 392,
+ "note": "Corrupti perferendis voluptas et iure omnis officia.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:01.160Z",
+ "updated_at": "2016-03-22T15:20:01.160Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 391,
+ "note": "Autem quo fugit in iste nesciunt tempora.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:01.131Z",
+ "updated_at": "2016-03-22T15:20:01.131Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 390,
+ "note": "Magni porro ut soluta quis et eveniet maiores.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:01.101Z",
+ "updated_at": "2016-03-22T15:20:01.101Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 389,
+ "note": "Sed consequuntur debitis nisi veniam exercitationem recusandae a quisquam.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:01.070Z",
+ "updated_at": "2016-03-22T15:20:01.070Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 388,
+ "note": "Aut impedit qui consectetur dicta temporibus.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:01.042Z",
+ "updated_at": "2016-03-22T15:20:01.042Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 387,
+ "note": "Officia repudiandae ut culpa ipsa reiciendis.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:01.005Z",
+ "updated_at": "2016-03-22T15:20:01.005Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 33,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 32,
+ "title": "Velit nihil est alias blanditiis eius earum autem hic.",
+ "assignee_id": 22,
+ "author_id": 26,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.225Z",
+ "updated_at": "2016-03-22T15:20:01.495Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Id voluptas ut sint aut laborum nobis commodi.",
+ "milestone_id": 11,
+ "state": "opened",
+ "iid": 2,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 402,
+ "note": "Magni ut eligendi sit sint recusandae voluptas tempore necessitatibus.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:01.489Z",
+ "updated_at": "2016-03-22T15:20:01.489Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 401,
+ "note": "Est repellat commodi incidunt tempore earum optio unde sint.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:01.455Z",
+ "updated_at": "2016-03-22T15:20:01.455Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 400,
+ "note": "Vero unde debitis tempore est laboriosam ut esse.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:01.421Z",
+ "updated_at": "2016-03-22T15:20:01.421Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 399,
+ "note": "Omnis qui asperiores expedita harum voluptatem eius.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:01.391Z",
+ "updated_at": "2016-03-22T15:20:01.391Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 398,
+ "note": "Dolorem doloribus delectus quo ratione esse veritatis.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:01.358Z",
+ "updated_at": "2016-03-22T15:20:01.358Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 397,
+ "note": "Quia esse et odit id est omnis dolorum quia.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:01.329Z",
+ "updated_at": "2016-03-22T15:20:01.329Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 396,
+ "note": "Exercitationem suscipit non rerum tempore sit.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:01.297Z",
+ "updated_at": "2016-03-22T15:20:01.297Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 395,
+ "note": "Nihil veniam magni sit officiis.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:01.268Z",
+ "updated_at": "2016-03-22T15:20:01.268Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 32,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ },
+ {
+ "id": 31,
+ "title": "Asperiores recusandae praesentium voluptas pariatur provident qui exercitationem quis.",
+ "assignee_id": 26,
+ "author_id": 24,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:26.889Z",
+ "updated_at": "2016-03-22T15:20:01.834Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Ex voluptates qui excepturi cupiditate.",
+ "milestone_id": 11,
+ "state": "closed",
+ "iid": 1,
+ "updated_by_id": null,
+ "confidential": false,
+ "deleted_at": null,
+ "moved_to_id": null,
+ "due_date": null,
+ "notes": [
+ {
+ "id": 410,
+ "note": "Sit itaque non nihil nisi qui voluptatem dolorem error.",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:01.828Z",
+ "updated_at": "2016-03-22T15:20:01.828Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 409,
+ "note": "Omnis rem nihil molestiae enim laudantium doloremque.",
+ "noteable_type": "Issue",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:01.783Z",
+ "updated_at": "2016-03-22T15:20:01.783Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 408,
+ "note": "Ullam harum sit et optio incidunt.",
+ "noteable_type": "Issue",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:01.746Z",
+ "updated_at": "2016-03-22T15:20:01.746Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 407,
+ "note": "Fugit distinctio ab quo ipsam.",
+ "noteable_type": "Issue",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:01.716Z",
+ "updated_at": "2016-03-22T15:20:01.716Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 406,
+ "note": "Impedit iste possimus ad ea.",
+ "noteable_type": "Issue",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:01.676Z",
+ "updated_at": "2016-03-22T15:20:01.676Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 405,
+ "note": "Nemo recusandae dolore distinctio quam consequuntur ut et aut.",
+ "noteable_type": "Issue",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:01.641Z",
+ "updated_at": "2016-03-22T15:20:01.641Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 404,
+ "note": "Nisi repudiandae repellat nulla culpa quasi expedita quod velit.",
+ "noteable_type": "Issue",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:01.601Z",
+ "updated_at": "2016-03-22T15:20:01.601Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 403,
+ "note": "Quibusdam odio temporibus nemo voluptatibus accusamus.",
+ "noteable_type": "Issue",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:01.552Z",
+ "updated_at": "2016-03-22T15:20:01.552Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 31,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ]
+ }
+ ],
+ "labels": [
+ {
+ "id": 12,
+ "title": "test",
+ "color": "#428bca",
+ "project_id": 5,
+ "created_at": "2016-05-10T10:53:14.214Z",
+ "updated_at": "2016-05-10T10:53:14.214Z",
+ "template": false,
+ "description": "test label"
+ }
+ ],
+ "milestones": [
+ {
+ "id": 11,
+ "title": "v2.0",
+ "project_id": 5,
+ "description": "Sapiente facilis architecto reprehenderit aut sed enim.",
+ "due_date": null,
+ "created_at": "2016-03-22T15:13:21.631Z",
+ "updated_at": "2016-03-22T15:13:21.631Z",
+ "state": "closed",
+ "iid": 2
+ },
+ {
+ "id": 10,
+ "title": "v1.0",
+ "project_id": 5,
+ "description": "Est sed eos minima veniam culpa aut non.",
+ "due_date": null,
+ "created_at": "2016-03-22T15:13:21.622Z",
+ "updated_at": "2016-03-22T15:13:21.622Z",
+ "state": "closed",
+ "iid": 1
+ }
+ ],
+ "snippets": [
+
+ ],
+ "releases": [
+
+ ],
+ "events": [
+ {
+ "id": 301,
+ "target_type": "Note",
+ "target_id": 1357,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-04-12T13:08:30.886Z",
+ "updated_at": "2016-04-12T13:08:30.886Z",
+ "action": 6,
+ "author_id": 1
+ },
+ {
+ "id": 227,
+ "target_type": "MergeRequest",
+ "target_id": 85,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:19:44.957Z",
+ "updated_at": "2016-03-22T15:19:44.957Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 226,
+ "target_type": "MergeRequest",
+ "target_id": 84,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:19:44.600Z",
+ "updated_at": "2016-03-22T15:19:44.600Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 157,
+ "target_type": "MergeRequest",
+ "target_id": 15,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:45.936Z",
+ "updated_at": "2016-03-22T15:13:45.936Z",
+ "action": 1,
+ "author_id": 3
+ },
+ {
+ "id": 156,
+ "target_type": "MergeRequest",
+ "target_id": 14,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:45.500Z",
+ "updated_at": "2016-03-22T15:13:45.500Z",
+ "action": 1,
+ "author_id": 10
+ },
+ {
+ "id": 155,
+ "target_type": "MergeRequest",
+ "target_id": 13,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:45.242Z",
+ "updated_at": "2016-03-22T15:13:45.242Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 154,
+ "target_type": "MergeRequest",
+ "target_id": 12,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:44.940Z",
+ "updated_at": "2016-03-22T15:13:44.940Z",
+ "action": 1,
+ "author_id": 24
+ },
+ {
+ "id": 153,
+ "target_type": "MergeRequest",
+ "target_id": 11,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:44.568Z",
+ "updated_at": "2016-03-22T15:13:44.568Z",
+ "action": 1,
+ "author_id": 26
+ },
+ {
+ "id": 152,
+ "target_type": "MergeRequest",
+ "target_id": 10,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:44.225Z",
+ "updated_at": "2016-03-22T15:13:44.225Z",
+ "action": 1,
+ "author_id": 22
+ },
+ {
+ "id": 151,
+ "target_type": "MergeRequest",
+ "target_id": 9,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:43.868Z",
+ "updated_at": "2016-03-22T15:13:43.868Z",
+ "action": 1,
+ "author_id": 24
+ },
+ {
+ "id": 102,
+ "target_type": "Issue",
+ "target_id": 40,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:28.474Z",
+ "updated_at": "2016-03-22T15:13:28.474Z",
+ "action": 1,
+ "author_id": 4
+ },
+ {
+ "id": 101,
+ "target_type": "Issue",
+ "target_id": 39,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:28.328Z",
+ "updated_at": "2016-03-22T15:13:28.328Z",
+ "action": 1,
+ "author_id": 12
+ },
+ {
+ "id": 100,
+ "target_type": "Issue",
+ "target_id": 38,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:28.204Z",
+ "updated_at": "2016-03-22T15:13:28.204Z",
+ "action": 1,
+ "author_id": 3
+ },
+ {
+ "id": 99,
+ "target_type": "Issue",
+ "target_id": 37,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:28.055Z",
+ "updated_at": "2016-03-22T15:13:28.055Z",
+ "action": 1,
+ "author_id": 10
+ },
+ {
+ "id": 98,
+ "target_type": "Issue",
+ "target_id": 36,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.913Z",
+ "updated_at": "2016-03-22T15:13:27.913Z",
+ "action": 1,
+ "author_id": 4
+ },
+ {
+ "id": 97,
+ "target_type": "Issue",
+ "target_id": 35,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.731Z",
+ "updated_at": "2016-03-22T15:13:27.731Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 96,
+ "target_type": "Issue",
+ "target_id": 34,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.564Z",
+ "updated_at": "2016-03-22T15:13:27.564Z",
+ "action": 1,
+ "author_id": 1
+ },
+ {
+ "id": 95,
+ "target_type": "Issue",
+ "target_id": 33,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.429Z",
+ "updated_at": "2016-03-22T15:13:27.429Z",
+ "action": 1,
+ "author_id": 22
+ },
+ {
+ "id": 94,
+ "target_type": "Issue",
+ "target_id": 32,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:27.287Z",
+ "updated_at": "2016-03-22T15:13:27.287Z",
+ "action": 1,
+ "author_id": 26
+ },
+ {
+ "id": 93,
+ "target_type": "Issue",
+ "target_id": 31,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:26.997Z",
+ "updated_at": "2016-03-22T15:13:26.997Z",
+ "action": 1,
+ "author_id": 24
+ },
+ {
+ "id": 51,
+ "target_type": "Milestone",
+ "target_id": 11,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:21.634Z",
+ "updated_at": "2016-03-22T15:13:21.634Z",
+ "action": 1,
+ "author_id": 26
+ },
+ {
+ "id": 50,
+ "target_type": "Milestone",
+ "target_id": 10,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:21.625Z",
+ "updated_at": "2016-03-22T15:13:21.625Z",
+ "action": 1,
+ "author_id": 22
+ },
+ {
+ "id": 24,
+ "target_type": null,
+ "target_id": null,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:20.750Z",
+ "updated_at": "2016-03-22T15:13:20.750Z",
+ "action": 8,
+ "author_id": 12
+ },
+ {
+ "id": 23,
+ "target_type": null,
+ "target_id": null,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:20.711Z",
+ "updated_at": "2016-03-22T15:13:20.711Z",
+ "action": 8,
+ "author_id": 22
+ },
+ {
+ "id": 22,
+ "target_type": null,
+ "target_id": null,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:20.667Z",
+ "updated_at": "2016-03-22T15:13:20.667Z",
+ "action": 8,
+ "author_id": 26
+ },
+ {
+ "id": 21,
+ "target_type": null,
+ "target_id": null,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:20.646Z",
+ "updated_at": "2016-03-22T15:13:20.646Z",
+ "action": 8,
+ "author_id": 1
+ },
+ {
+ "id": 5,
+ "target_type": null,
+ "target_id": null,
+ "title": null,
+ "data": null,
+ "project_id": 5,
+ "created_at": "2016-03-22T15:13:10.369Z",
+ "updated_at": "2016-03-22T15:13:10.369Z",
+ "action": 1,
+ "author_id": 1
+ }
+ ],
+ "project_members": [
+ {
+ "id": 35,
+ "access_level": 40,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 12,
+ "notification_level": 3,
+ "created_at": "2016-03-22T15:13:20.743Z",
+ "updated_at": "2016-03-22T15:13:20.743Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "user": {
+ "id": 12,
+ "email": "maureen.bogisich@russelkessler.com",
+ "username": "evans"
+ }
+ },
+ {
+ "id": 34,
+ "access_level": 40,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 22,
+ "notification_level": 3,
+ "created_at": "2016-03-22T15:13:20.708Z",
+ "updated_at": "2016-03-22T15:13:20.708Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "user": {
+ "id": 22,
+ "email": "user0@example.com",
+ "username": "user0"
+ }
+ },
+ {
+ "id": 33,
+ "access_level": 40,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 26,
+ "notification_level": 3,
+ "created_at": "2016-03-22T15:13:20.664Z",
+ "updated_at": "2016-03-22T15:13:20.664Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "user": {
+ "id": 26,
+ "email": "user4@example.com",
+ "username": "user4"
+ }
+ },
+ {
+ "id": 32,
+ "access_level": 20,
+ "source_id": 5,
+ "source_type": "Project",
+ "user_id": 1,
+ "notification_level": 3,
+ "created_at": "2016-03-22T15:13:20.643Z",
+ "updated_at": "2016-03-22T15:13:20.643Z",
+ "created_by_id": null,
+ "invite_email": null,
+ "invite_token": null,
+ "invite_accepted_at": null,
+ "user": {
+ "id": 1,
+ "email": "nospam@bluegod.net",
+ "username": "root"
+ }
+ }
+ ],
+ "merge_requests": [
+ {
+ "id": 85,
+ "target_branch": "feature",
+ "source_branch": "feature_conflict",
+ "source_project_id": 5,
+ "author_id": 1,
+ "assignee_id": null,
+ "title": "Cannot be automatically merged",
+ "created_at": "2016-03-22T15:19:44.807Z",
+ "updated_at": "2016-03-22T15:20:09.557Z",
+ "milestone_id": null,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 9,
+ "description": null,
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 638,
+ "note": "Ab velit ducimus totam sunt ut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:09.553Z",
+ "updated_at": "2016-03-22T15:20:09.553Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 637,
+ "note": "Ipsum aliquam est in unde similique nihil illo ea.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:09.528Z",
+ "updated_at": "2016-03-22T15:20:09.528Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 636,
+ "note": "Soluta inventore adipisci et consequatur expedita aliquid earum modi.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:09.496Z",
+ "updated_at": "2016-03-22T15:20:09.496Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 635,
+ "note": "Corporis incidunt tempore est deleniti.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:09.469Z",
+ "updated_at": "2016-03-22T15:20:09.469Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 634,
+ "note": "Hic dolores voluptatibus qui necessitatibus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:09.440Z",
+ "updated_at": "2016-03-22T15:20:09.440Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 633,
+ "note": "Rerum architecto placeat doloribus voluptates consequuntur quo.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:09.412Z",
+ "updated_at": "2016-03-22T15:20:09.412Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 632,
+ "note": "Vel earum aut ut occaecati aut ut rerum qui.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:09.389Z",
+ "updated_at": "2016-03-22T15:20:09.389Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 631,
+ "note": "Est voluptatibus dolores animi numquam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:09.361Z",
+ "updated_at": "2016-03-22T15:20:09.361Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 85,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 85,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
+ "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
+ ],
+ "authored_date": "2014-08-06T08:35:52.000+02:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-08-06T08:35:52.000+02:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
+ ],
+ "authored_date": "2014-02-27T10:01:38.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T10:01:38.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ ],
+ "authored_date": "2014-02-27T09:57:31.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:57:31.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "d14d6c0abdd253381df51a723d58691b2ee1ab08"
+ ],
+ "authored_date": "2014-02-27T09:54:21.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:54:21.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "c1acaa58bbcbc3eafe538cb8274ba387047b69f8"
+ ],
+ "authored_date": "2014-02-27T09:49:50.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:49:50.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ },
+ {
+ "id": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
+ ],
+ "authored_date": "2014-02-27T09:48:32.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:48:32.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "Binary files a/.DS_Store and /dev/null differ\n",
+ "new_path": ".DS_Store",
+ "old_path": ".DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n",
+ "new_path": ".gitignore",
+ "old_path": ".gitignore",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n",
+ "new_path": ".gitmodules",
+ "old_path": ".gitmodules",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "Binary files a/files/.DS_Store and /dev/null differ\n",
+ "new_path": "files/.DS_Store",
+ "old_path": "files/.DS_Store",
+ "a_mode": "100644",
+ "b_mode": "0",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": true,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n",
+ "new_path": "files/ruby/feature.rb",
+ "old_path": "files/ruby/feature.rb",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "new_path": "files/ruby/popen.rb",
+ "old_path": "files/ruby/popen.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n",
+ "new_path": "files/ruby/regex.rb",
+ "old_path": "files/ruby/regex.rb",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n",
+ "new_path": "gitlab-grack",
+ "old_path": "gitlab-grack",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n",
+ "new_path": "gitlab-shell",
+ "old_path": "gitlab-shell",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 85,
+ "created_at": "2016-03-22T15:19:44.810Z",
+ "updated_at": "2016-03-22T15:19:44.901Z",
+ "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
+ "real_size": "9"
+ }
+ },
+ {
+ "id": 84,
+ "target_branch": "master",
+ "source_branch": "feature",
+ "source_project_id": 5,
+ "author_id": 1,
+ "assignee_id": null,
+ "title": "Can be automatically merged",
+ "created_at": "2016-03-22T15:19:44.482Z",
+ "updated_at": "2016-03-22T15:20:09.773Z",
+ "milestone_id": null,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 8,
+ "description": null,
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 646,
+ "note": "Temporibus debitis veniam est ut sit nihil.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:09.770Z",
+ "updated_at": "2016-03-22T15:20:09.770Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 645,
+ "note": "Ut assumenda dignissimos quibusdam veritatis sequi dolores.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:09.740Z",
+ "updated_at": "2016-03-22T15:20:09.740Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 644,
+ "note": "Velit quae quidem cupiditate laudantium nihil ut eveniet.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:09.717Z",
+ "updated_at": "2016-03-22T15:20:09.717Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 643,
+ "note": "Repellat quas porro sed mollitia laborum ut fugiat.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:09.690Z",
+ "updated_at": "2016-03-22T15:20:09.690Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 642,
+ "note": "Qui aut debitis perspiciatis et voluptatem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:09.665Z",
+ "updated_at": "2016-03-22T15:20:09.665Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 641,
+ "note": "Quia id quia velit et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:09.639Z",
+ "updated_at": "2016-03-22T15:20:09.639Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 640,
+ "note": "Corporis commodi doloremque itaque non animi.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:09.617Z",
+ "updated_at": "2016-03-22T15:20:09.617Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 639,
+ "note": "Possimus dignissimos voluptatum in tenetur.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:09.589Z",
+ "updated_at": "2016-03-22T15:20:09.589Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 84,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 84,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "parent_ids": [
+ "ae73cb07c9eeaf35924a10f713b364d32b2dd34f"
+ ],
+ "authored_date": "2014-02-27T09:26:01.000+01:00",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "committed_date": "2014-02-27T09:26:01.000+01:00",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dmitriy.zaporozhets@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
+ "new_path": "files/ruby/feature.rb",
+ "old_path": "files/ruby/feature.rb",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 84,
+ "created_at": "2016-03-22T15:19:44.485Z",
+ "updated_at": "2016-03-22T15:19:44.577Z",
+ "base_commit_sha": "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
+ "real_size": "1"
+ }
+ },
+ {
+ "id": 15,
+ "target_branch": "markdown",
+ "source_branch": "master",
+ "source_project_id": 5,
+ "author_id": 3,
+ "assignee_id": 3,
+ "title": "Nulla explicabo iure voluptas perferendis autem autem unde nemo totam optio.",
+ "created_at": "2016-03-22T15:13:45.689Z",
+ "updated_at": "2016-03-22T15:20:30.476Z",
+ "milestone_id": 10,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 7,
+ "description": "Doloribus dignissimos impedit qui et provident exercitationem. Veniam quis magni qui fugiat. Et quia voluptate et vel consequatur pariatur ea est.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 1231,
+ "note": "Rerum optio quibusdam provident possimus quis cum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:30.472Z",
+ "updated_at": "2016-03-22T15:20:30.472Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 1230,
+ "note": "Quasi odit repudiandae ut officiis ut nihil illo.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:30.444Z",
+ "updated_at": "2016-03-22T15:20:30.444Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 1229,
+ "note": "Aut vero dolores facere sed.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:30.412Z",
+ "updated_at": "2016-03-22T15:20:30.412Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 1228,
+ "note": "Autem voluptatem et blanditiis accusantium deserunt et et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:30.383Z",
+ "updated_at": "2016-03-22T15:20:30.383Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 1227,
+ "note": "Voluptatem aliquam voluptatem molestiae est.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:30.352Z",
+ "updated_at": "2016-03-22T15:20:30.352Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 1226,
+ "note": "Ea aut cupiditate est consequatur animi error qui et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:30.319Z",
+ "updated_at": "2016-03-22T15:20:30.319Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 1225,
+ "note": "Voluptates est voluptas et nostrum modi beatae inventore et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:30.289Z",
+ "updated_at": "2016-03-22T15:20:30.289Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 1224,
+ "note": "Quia est rerum adipisci cupiditate.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:30.260Z",
+ "updated_at": "2016-03-22T15:20:30.260Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 15,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 15,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "message": "Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "048721d90c449b244b7b4c53a9186b04330174ec"
+ ],
+ "authored_date": "2015-12-07T12:52:12.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "marin@gitlab.com",
+ "committed_date": "2015-12-07T12:52:12.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "marin@gitlab.com"
+ },
+ {
+ "id": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "message": "LFS object pointer.\n",
+ "parent_ids": [
+ "5f923865dde3436854e9ceb9cdb7815618d4e849"
+ ],
+ "authored_date": "2015-12-07T11:54:28.000+01:00",
+ "author_name": "Marin Jankovski",
+ "author_email": "maxlazio@gmail.com",
+ "committed_date": "2015-12-07T11:54:28.000+01:00",
+ "committer_name": "Marin Jankovski",
+ "committer_email": "maxlazio@gmail.com"
+ },
+ {
+ "id": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "message": "GitLab currently doesn't support patches that involve a merge commit: add a commit here\n",
+ "parent_ids": [
+ "d2d430676773caa88cdaf7c55944073b2fd5561a"
+ ],
+ "authored_date": "2015-11-13T16:27:12.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T16:27:12.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "message": "Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
+ ],
+ "authored_date": "2015-11-13T08:50:17.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:50:17.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "message": "Add GitLab SVG\n",
+ "parent_ids": [
+ "59e29889be61e6e0e5e223bfa9ac2721d31605b8"
+ ],
+ "authored_date": "2015-11-13T08:39:43.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T08:39:43.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "59e29889be61e6e0e5e223bfa9ac2721d31605b8",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4",
+ "parent_ids": [
+ "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "66eceea0db202bb39c4e445e8ca28689645366c5"
+ ],
+ "authored_date": "2015-11-13T07:21:40.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T07:21:40.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "66eceea0db202bb39c4e445e8ca28689645366c5",
+ "message": "add spaces in whitespace file\n",
+ "parent_ids": [
+ "08f22f255f082689c0d7d39d19205085311542bc"
+ ],
+ "authored_date": "2015-11-13T06:01:27.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:01:27.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "08f22f255f082689c0d7d39d19205085311542bc",
+ "message": "remove emtpy file.(beacase git ignore empty file)\nadd whitespace test file.\n",
+ "parent_ids": [
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T06:00:16.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T06:00:16.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "19e2e9b4ef76b422ce1154af39a91323ccc57434",
+ "message": "Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "c642fe9b8b9f28f9225d7ea953fe14e74748d53b"
+ ],
+ "authored_date": "2015-11-13T05:23:14.000+01:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@gmail.com",
+ "committed_date": "2015-11-13T05:23:14.000+01:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@gmail.com"
+ },
+ {
+ "id": "c642fe9b8b9f28f9225d7ea953fe14e74748d53b",
+ "message": "add whitespace in empty\n",
+ "parent_ids": [
+ "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0"
+ ],
+ "authored_date": "2015-11-13T05:08:45.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:45.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "9a944d90955aaf45f6d0c88f30e27f8d2c41cec0",
+ "message": "add empty file\n",
+ "parent_ids": [
+ "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd"
+ ],
+ "authored_date": "2015-11-13T05:08:04.000+01:00",
+ "author_name": "윤민식",
+ "author_email": "minsik.yoon@samsung.com",
+ "committed_date": "2015-11-13T05:08:04.000+01:00",
+ "committer_name": "윤민식",
+ "committer_email": "minsik.yoon@samsung.com"
+ },
+ {
+ "id": "c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+ "message": "Add ISO-8859 test file\n",
+ "parent_ids": [
+ "e56497bb5f03a90a51293fc6d516788730953899"
+ ],
+ "authored_date": "2015-08-25T17:53:12.000+02:00",
+ "author_name": "Stan Hu",
+ "author_email": "stanhu@packetzoom.com",
+ "committed_date": "2015-08-25T17:53:12.000+02:00",
+ "committer_name": "Stan Hu",
+ "committer_email": "stanhu@packetzoom.com"
+ },
+ {
+ "id": "e56497bb5f03a90a51293fc6d516788730953899",
+ "message": "Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)\n\nSee merge request !2\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "4cd80ccab63c82b4bad16faa5193fbd2aa06df40"
+ ],
+ "authored_date": "2015-01-10T22:23:29.000+01:00",
+ "author_name": "Sytse Sijbrandij",
+ "author_email": "sytse@gitlab.com",
+ "committed_date": "2015-01-10T22:23:29.000+01:00",
+ "committer_name": "Sytse Sijbrandij",
+ "committer_email": "sytse@gitlab.com"
+ },
+ {
+ "id": "4cd80ccab63c82b4bad16faa5193fbd2aa06df40",
+ "message": "add directory structure for tree_helper spec\n",
+ "parent_ids": [
+ "5937ac0a7beb003549fc5fd26fc247adbce4a52e"
+ ],
+ "authored_date": "2015-01-10T21:28:18.000+01:00",
+ "author_name": "marmis85",
+ "author_email": "marmis85@gmail.com",
+ "committed_date": "2015-01-10T21:28:18.000+01:00",
+ "committer_name": "marmis85",
+ "committer_email": "marmis85@gmail.com"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
+ "new_path": "CHANGELOG",
+ "old_path": "CHANGELOG",
+ "a_mode": "100644",
+ "b_mode": "100644",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n",
+ "new_path": "encoding/iso8859.txt",
+ "old_path": "encoding/iso8859.txt",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "new_path": "files/images/wm.svg",
+ "old_path": "files/images/wm.svg",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n",
+ "new_path": "files/lfs/lfs_object.iso",
+ "old_path": "files/lfs/lfs_object.iso",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n",
+ "new_path": "files/whitespace",
+ "old_path": "files/whitespace",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ },
+ {
+ "diff": "--- /dev/null\n+++ b/foo/bar/.gitkeep\n",
+ "new_path": "foo/bar/.gitkeep",
+ "old_path": "foo/bar/.gitkeep",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 15,
+ "created_at": "2016-03-22T15:13:45.692Z",
+ "updated_at": "2016-03-22T15:13:45.808Z",
+ "base_commit_sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "real_size": "6"
+ }
+ },
+ {
+ "id": 14,
+ "target_branch": "test-1",
+ "source_branch": "test-10",
+ "source_project_id": 5,
+ "author_id": 10,
+ "assignee_id": 1,
+ "title": "Tempore aliquid sit amet odit qui cum iusto voluptatibus asperiores.",
+ "created_at": "2016-03-22T15:13:45.442Z",
+ "updated_at": "2016-03-22T15:20:30.735Z",
+ "milestone_id": 10,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 6,
+ "description": "Quis et et autem saepe ut. Eum corporis tempore cum dolore. Molestiae pariatur voluptatem officia perferendis aut veniam.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 1239,
+ "note": "Aspernatur suscipit veritatis aliquid rerum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:30.731Z",
+ "updated_at": "2016-03-22T15:20:30.731Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 1238,
+ "note": "Rerum deleniti omnis porro commodi.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:30.701Z",
+ "updated_at": "2016-03-22T15:20:30.701Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 1237,
+ "note": "Eaque ut magnam rerum non dolores esse.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:30.667Z",
+ "updated_at": "2016-03-22T15:20:30.667Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 1236,
+ "note": "Fugit et aut similique illum ut natus maiores et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:30.637Z",
+ "updated_at": "2016-03-22T15:20:30.637Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 1235,
+ "note": "Qui qui temporibus eos aliquam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:30.608Z",
+ "updated_at": "2016-03-22T15:20:30.608Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 1234,
+ "note": "Voluptates hic dolorum aut inventore.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:30.575Z",
+ "updated_at": "2016-03-22T15:20:30.575Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 1233,
+ "note": "Dolorum iure at dolor dolores numquam iusto.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:30.548Z",
+ "updated_at": "2016-03-22T15:20:30.548Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 1232,
+ "note": "Nihil est eum aspernatur amet minus et corporis consectetur.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:30.517Z",
+ "updated_at": "2016-03-22T15:20:30.517Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 14,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 14,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "bce96ecee98f51fa5d91021e6c42859a35a701ad",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T15:40:05.000+01:00",
+ "author_name": "Test Lopez",
+ "author_email": "Test@Testlopez.es",
+ "committed_date": "2016-01-19T15:40:05.000+01:00",
+ "committer_name": "Test Lopez",
+ "committer_email": "Test@Testlopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 14,
+ "created_at": "2016-03-22T15:13:45.444Z",
+ "updated_at": "2016-03-22T15:13:45.486Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ }
+ },
+ {
+ "id": 13,
+ "target_branch": "test-11",
+ "source_branch": "test-12",
+ "source_project_id": 5,
+ "author_id": 1,
+ "assignee_id": 26,
+ "title": "Voluptas minus sunt voluptatum quis quia ut velit distinctio itaque.",
+ "created_at": "2016-03-22T15:13:45.164Z",
+ "updated_at": "2016-03-22T15:20:30.994Z",
+ "milestone_id": 11,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 5,
+ "description": "Ea ut modi consectetur et minus beatae. Et sunt ducimus praesentium libero officia maiores voluptas cumque. Rerum in aut corporis et ullam omnis.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 1247,
+ "note": "Non error magnam placeat cupiditate eum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:30.989Z",
+ "updated_at": "2016-03-22T15:20:30.989Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 1246,
+ "note": "Eos optio et architecto eligendi ea est nihil.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:30.957Z",
+ "updated_at": "2016-03-22T15:20:30.957Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 1245,
+ "note": "Reprehenderit in atque dolor et repudiandae a est.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:30.928Z",
+ "updated_at": "2016-03-22T15:20:30.928Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 1244,
+ "note": "Numquam fugit doloremque iure odio et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:30.902Z",
+ "updated_at": "2016-03-22T15:20:30.902Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 1243,
+ "note": "Doloribus laboriosam id harum voluptatum vitae ut quam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:30.863Z",
+ "updated_at": "2016-03-22T15:20:30.863Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 1242,
+ "note": "Harum et ut ipsum dolore ea.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:30.832Z",
+ "updated_at": "2016-03-22T15:20:30.832Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 1241,
+ "note": "Corporis sed soluta ut est modi natus ab.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:30.802Z",
+ "updated_at": "2016-03-22T15:20:30.802Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 1240,
+ "note": "Corrupti totam tenetur officiis ratione dolores est qui vel.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:30.771Z",
+ "updated_at": "2016-03-22T15:20:30.771Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 13,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 13,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "a4e5dfebf42e34596526acb8611bc7ed80e4eb3f",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T15:44:02.000+01:00",
+ "author_name": "Test Lopez",
+ "author_email": "Test@Testlopez.es",
+ "committed_date": "2016-01-19T15:44:02.000+01:00",
+ "committer_name": "Test Lopez",
+ "committer_email": "Test@Testlopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 13,
+ "created_at": "2016-03-22T15:13:45.167Z",
+ "updated_at": "2016-03-22T15:13:45.216Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ }
+ },
+ {
+ "id": 12,
+ "target_branch": "test-15",
+ "source_branch": "test-2",
+ "source_project_id": 5,
+ "author_id": 24,
+ "assignee_id": 12,
+ "title": "In assumenda nam quaerat qui eos sit facilis enim quia quis.",
+ "created_at": "2016-03-22T15:13:44.837Z",
+ "updated_at": "2016-03-22T15:20:31.258Z",
+ "milestone_id": 10,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 4,
+ "description": "Soluta excepturi quis iste vero delectus rerum. Consequatur possimus aliquam necessitatibus deleniti rerum est impedit. Eius rem et consequatur assumenda est commodi.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 1255,
+ "note": "Quibusdam rem aut similique ipsum recusandae ut accusamus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:31.253Z",
+ "updated_at": "2016-03-22T15:20:31.253Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 1254,
+ "note": "Cumque sed omnis ipsa et magnam dolorem et.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:31.224Z",
+ "updated_at": "2016-03-22T15:20:31.224Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 1253,
+ "note": "Molestiae beatae id consequatur nam minus quia.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:31.195Z",
+ "updated_at": "2016-03-22T15:20:31.195Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 1252,
+ "note": "Voluptatem dolorem dignissimos itaque tempora quas ut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:31.166Z",
+ "updated_at": "2016-03-22T15:20:31.166Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 1251,
+ "note": "Debitis qui quibusdam voluptas repellat veritatis dicta rerum id.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:31.137Z",
+ "updated_at": "2016-03-22T15:20:31.137Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 1250,
+ "note": "Suscipit optio ad voluptatem dignissimos temporibus amet molestias ut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:31.107Z",
+ "updated_at": "2016-03-22T15:20:31.107Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 1249,
+ "note": "Nemo aut vitae et ducimus autem ex dolores.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:31.073Z",
+ "updated_at": "2016-03-22T15:20:31.073Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 1248,
+ "note": "Repellendus eaque ex molestiae laudantium placeat quidem vitae recusandae.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:31.038Z",
+ "updated_at": "2016-03-22T15:20:31.038Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 12,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 12,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "97a0df9696e2aebf10c31b3016f40214e0e8f243",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T14:08:21.000+01:00",
+ "author_name": "Test Lopez",
+ "author_email": "Test@Testlopez.es",
+ "committed_date": "2016-01-19T14:08:21.000+01:00",
+ "committer_name": "Test Lopez",
+ "committer_email": "Test@Testlopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 12,
+ "created_at": "2016-03-22T15:13:44.840Z",
+ "updated_at": "2016-03-22T15:13:44.908Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ }
+ },
+ {
+ "id": 11,
+ "target_branch": "test-3",
+ "source_branch": "test-5",
+ "source_project_id": 5,
+ "author_id": 26,
+ "assignee_id": 12,
+ "title": "Magni aut reprehenderit ut accusantium est eum.",
+ "created_at": "2016-03-22T15:13:44.494Z",
+ "updated_at": "2016-03-22T15:20:31.886Z",
+ "milestone_id": 10,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 3,
+ "description": "Et hic maxime harum ullam. Nulla velit pariatur libero recusandae. Dolor est earum laboriosam harum quo.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 1263,
+ "note": "Beatae incidunt exercitationem voluptates recusandae fuga quia enim.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:31.883Z",
+ "updated_at": "2016-03-22T15:20:31.883Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 1262,
+ "note": "Illum sunt id consequuntur fugit et quo ullam eum.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:31.860Z",
+ "updated_at": "2016-03-22T15:20:31.860Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 1261,
+ "note": "Alias reiciendis autem ipsa sequi autem nemo odio.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:31.456Z",
+ "updated_at": "2016-03-22T15:20:31.456Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 1260,
+ "note": "Maxime nisi odit eos nulla vel ex accusamus velit.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:31.426Z",
+ "updated_at": "2016-03-22T15:20:31.426Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 1259,
+ "note": "Excepturi et qui sapiente ut ducimus sunt nesciunt.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:31.397Z",
+ "updated_at": "2016-03-22T15:20:31.397Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 1258,
+ "note": "Quis rerum dolores et dolorem modi neque ullam doloribus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:31.364Z",
+ "updated_at": "2016-03-22T15:20:31.364Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 1257,
+ "note": "Voluptatum et mollitia neque aut.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:31.328Z",
+ "updated_at": "2016-03-22T15:20:31.328Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 1256,
+ "note": "Rerum laudantium dolor natus doloribus voluptas aliquid a.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:31.298Z",
+ "updated_at": "2016-03-22T15:20:31.298Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 11,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 11,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "f998ac87ac9244f15e9c15109a6f4e62a54b779d",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T14:43:23.000+01:00",
+ "author_name": "Test Lopez",
+ "author_email": "Test@Testlopez.es",
+ "committed_date": "2016-01-19T14:43:23.000+01:00",
+ "committer_name": "Test Lopez",
+ "committer_email": "Test@Testlopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 11,
+ "created_at": "2016-03-22T15:13:44.497Z",
+ "updated_at": "2016-03-22T15:13:44.547Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ }
+ },
+ {
+ "id": 10,
+ "target_branch": "test-6",
+ "source_branch": "test-7",
+ "source_project_id": 5,
+ "author_id": 22,
+ "assignee_id": 4,
+ "title": "Rerum commodi corporis quis qui fugit sed ut.",
+ "created_at": "2016-03-22T15:13:44.103Z",
+ "updated_at": "2016-03-22T15:20:32.096Z",
+ "milestone_id": 11,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 2,
+ "description": "Laudantium vel dignissimos aspernatur quis aut. Dolores et doloremque ipsa quia voluptate modi labore. Ipsa provident repellat error et nihil.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 1271,
+ "note": "Quod ut ut quisquam et ut dolorem dolor.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:32.093Z",
+ "updated_at": "2016-03-22T15:20:32.093Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 1270,
+ "note": "Sed deserunt et explicabo rem repellat voluptatem.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:32.070Z",
+ "updated_at": "2016-03-22T15:20:32.070Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 1269,
+ "note": "Veritatis architecto omnis consequatur et optio.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:32.046Z",
+ "updated_at": "2016-03-22T15:20:32.046Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 1268,
+ "note": "Omnis suscipit odio molestiae debitis quia autem magni.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:32.019Z",
+ "updated_at": "2016-03-22T15:20:32.019Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 1267,
+ "note": "Molestias est sunt est tempora consequatur cupiditate magnam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:31.993Z",
+ "updated_at": "2016-03-22T15:20:31.993Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 1266,
+ "note": "Ratione blanditiis eveniet voluptatem nostrum rerum excepturi in molestiae.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:31.969Z",
+ "updated_at": "2016-03-22T15:20:31.969Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 1265,
+ "note": "Illo voluptatibus vel odio ea.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:31.944Z",
+ "updated_at": "2016-03-22T15:20:31.944Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 1264,
+ "note": "Earum veritatis quis facere itaque iure.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:31.919Z",
+ "updated_at": "2016-03-22T15:20:31.919Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 10,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 10,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "b42bb86cea49bdcef943e521584b7f417d8ddd3d",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T15:03:09.000+01:00",
+ "author_name": "Test Lopez",
+ "author_email": "Test@Testlopez.es",
+ "committed_date": "2016-01-19T15:03:09.000+01:00",
+ "committer_name": "Test Lopez",
+ "committer_email": "Test@Testlopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 10,
+ "created_at": "2016-03-22T15:13:44.107Z",
+ "updated_at": "2016-03-22T15:13:44.190Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ }
+ },
+ {
+ "id": 9,
+ "target_branch": "test-8",
+ "source_branch": "test-9",
+ "source_project_id": 5,
+ "author_id": 24,
+ "assignee_id": 3,
+ "title": "Saepe et neque ut vero nobis et voluptatum facere qui minima.",
+ "created_at": "2016-03-22T15:13:43.792Z",
+ "updated_at": "2016-03-22T15:20:32.309Z",
+ "milestone_id": 10,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 5,
+ "iid": 1,
+ "description": "Autem enim aliquam labore qui voluptas ut voluptatem. Et corrupti sit fuga dolores alias iusto voluptatem. Excepturi ut saepe accusamus neque distinctio.",
+ "position": 0,
+ "locked_at": null,
+ "updated_by_id": null,
+ "merge_error": null,
+ "merge_params": {
+
+ },
+ "merge_when_build_succeeds": false,
+ "merge_user_id": null,
+ "merge_commit_sha": null,
+ "deleted_at": null,
+ "notes": [
+ {
+ "id": 1279,
+ "note": "A corrupti nesciunt pariatur ea.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2016-03-22T15:20:32.307Z",
+ "updated_at": "2016-03-22T15:20:32.307Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Administrator"
+ }
+ },
+ {
+ "id": 1278,
+ "note": "Adipisci aut ut et voluptate numquam.",
+ "noteable_type": "MergeRequest",
+ "author_id": 3,
+ "created_at": "2016-03-22T15:20:32.281Z",
+ "updated_at": "2016-03-22T15:20:32.281Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Alexie Trantow"
+ }
+ },
+ {
+ "id": 1277,
+ "note": "Adipisci voluptatem quod ut placeat repellendus deleniti.",
+ "noteable_type": "MergeRequest",
+ "author_id": 4,
+ "created_at": "2016-03-22T15:20:32.255Z",
+ "updated_at": "2016-03-22T15:20:32.255Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Julius Moore"
+ }
+ },
+ {
+ "id": 1276,
+ "note": "Vitae et doloremque aut et aspernatur velit placeat sed.",
+ "noteable_type": "MergeRequest",
+ "author_id": 10,
+ "created_at": "2016-03-22T15:20:32.230Z",
+ "updated_at": "2016-03-22T15:20:32.230Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Robyn McCullough Jr."
+ }
+ },
+ {
+ "id": 1275,
+ "note": "Quos cupiditate nesciunt expedita aspernatur.",
+ "noteable_type": "MergeRequest",
+ "author_id": 12,
+ "created_at": "2016-03-22T15:20:32.207Z",
+ "updated_at": "2016-03-22T15:20:32.207Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "Vladimir McCullough"
+ }
+ },
+ {
+ "id": 1274,
+ "note": "Optio rem inventore dicta praesentium sit.",
+ "noteable_type": "MergeRequest",
+ "author_id": 22,
+ "created_at": "2016-03-22T15:20:32.181Z",
+ "updated_at": "2016-03-22T15:20:32.181Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 0"
+ }
+ },
+ {
+ "id": 1273,
+ "note": "Sit incidunt molestiae maxime officiis rerum necessitatibus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 24,
+ "created_at": "2016-03-22T15:20:32.159Z",
+ "updated_at": "2016-03-22T15:20:32.159Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 2"
+ }
+ },
+ {
+ "id": 1272,
+ "note": "Autem ut non itaque molestiae nisi quia officiis doloribus.",
+ "noteable_type": "MergeRequest",
+ "author_id": 26,
+ "created_at": "2016-03-22T15:20:32.129Z",
+ "updated_at": "2016-03-22T15:20:32.129Z",
+ "project_id": 5,
+ "attachment": {
+ "url": null
+ },
+ "line_code": null,
+ "commit_id": null,
+ "noteable_id": 9,
+ "system": false,
+ "st_diff": null,
+ "updated_by_id": null,
+ "author": {
+ "name": "User 4"
+ }
+ }
+ ],
+ "merge_request_diff": {
+ "id": 9,
+ "state": "collected",
+ "st_commits": [
+ {
+ "id": "e239ba8c97b80b2874579a4d625ea9628f4c8ff5",
+ "message": "fixes #10\n",
+ "parent_ids": [
+ "be93687618e4b132087f430a4d8fc3a609c9b77c"
+ ],
+ "authored_date": "2016-01-19T15:38:06.000+01:00",
+ "author_name": "Test Lopez",
+ "author_email": "Test@Testlopez.es",
+ "committed_date": "2016-01-19T15:38:06.000+01:00",
+ "committer_name": "Test Lopez",
+ "committer_email": "Test@Testlopez.es"
+ }
+ ],
+ "st_diffs": [
+ {
+ "diff": "--- /dev/null\n+++ b/test\n",
+ "new_path": "test",
+ "old_path": "test",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false,
+ "too_large": false
+ }
+ ],
+ "merge_request_id": 9,
+ "created_at": "2016-03-22T15:13:43.794Z",
+ "updated_at": "2016-03-22T15:13:43.848Z",
+ "base_commit_sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "real_size": "1"
+ }
+ }
+ ],
+ "pipelines": [
+ {
+ "id": 36,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.755Z",
+ "updated_at": "2016-03-22T15:20:35.755Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 71,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": "2016-03-29T06:28:12.630Z",
+ "trace": null,
+ "created_at": "2016-03-22T15:20:35.772Z",
+ "updated_at": "2016-03-29T06:28:12.634Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 36,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 72,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.",
+ "created_at": "2016-03-22T15:20:35.777Z",
+ "updated_at": "2016-03-22T15:20:35.777Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 36,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 37,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.757Z",
+ "updated_at": "2016-03-22T15:20:35.757Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 74,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.",
+ "created_at": "2016-03-22T15:20:35.846Z",
+ "updated_at": "2016-03-22T15:20:35.846Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 37,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 73,
+ "project_id": 5,
+ "status": "canceled",
+ "finished_at": null,
+ "trace": null,
+ "created_at": "2016-03-22T15:20:35.842Z",
+ "updated_at": "2016-03-22T15:20:35.842Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 37,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 38,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.759Z",
+ "updated_at": "2016-03-22T15:20:35.759Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 76,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.",
+ "created_at": "2016-03-22T15:20:35.882Z",
+ "updated_at": "2016-03-22T15:20:35.882Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 38,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 75,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": null,
+ "trace": "Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.",
+ "created_at": "2016-03-22T15:20:35.864Z",
+ "updated_at": "2016-03-22T15:20:35.864Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 38,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 39,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "d2d430676773caa88cdaf7c55944073b2fd5561a",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.761Z",
+ "updated_at": "2016-03-22T15:20:35.761Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 78,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.",
+ "created_at": "2016-03-22T15:20:35.927Z",
+ "updated_at": "2016-03-22T15:20:35.927Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 39,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 77,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": null,
+ "trace": "Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.",
+ "created_at": "2016-03-22T15:20:35.905Z",
+ "updated_at": "2016-03-22T15:20:35.905Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 39,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ },
+ {
+ "id": 40,
+ "project_id": 5,
+ "ref": "master",
+ "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
+ "before_sha": null,
+ "push_data": null,
+ "created_at": "2016-03-22T15:20:35.763Z",
+ "updated_at": "2016-03-22T15:20:35.763Z",
+ "tag": null,
+ "yaml_errors": null,
+ "committed_at": null,
+ "gl_project_id": 5,
+ "status": "failed",
+ "started_at": null,
+ "finished_at": null,
+ "duration": null,
+ "statuses": [
+ {
+ "id": 79,
+ "project_id": 5,
+ "status": "failed",
+ "finished_at": "2016-03-29T06:28:12.695Z",
+ "trace": "Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.",
+ "created_at": "2016-03-22T15:20:35.950Z",
+ "updated_at": "2016-03-29T06:28:12.696Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 40,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 1",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": null
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": null
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ },
+ {
+ "id": 80,
+ "project_id": 5,
+ "status": "success",
+ "finished_at": null,
+ "trace": "Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.",
+ "created_at": "2016-03-22T15:20:35.966Z",
+ "updated_at": "2016-03-22T15:20:35.966Z",
+ "started_at": null,
+ "runner_id": null,
+ "coverage": null,
+ "commit_id": 40,
+ "commands": "$ build command",
+ "job_id": null,
+ "name": "test build 2",
+ "deploy": false,
+ "options": null,
+ "allow_failure": false,
+ "stage": "test",
+ "trigger_request_id": null,
+ "stage_idx": 1,
+ "tag": null,
+ "ref": "master",
+ "user_id": null,
+ "target_url": null,
+ "description": null,
+ "artifacts_file": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
+ },
+ "gl_project_id": 5,
+ "artifacts_metadata": {
+ "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
+ },
+ "erased_by_id": null,
+ "erased_at": null
+ }
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
new file mode 100644
index 00000000000..7a40a43f8ae
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
+ describe 'restore project tree' do
+
+ let(:user) { create(:user) }
+ let(:namespace) { create(:namespace, owner: user) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
+ let(:project) { create(:empty_project, name: 'project', path: 'project') }
+ let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
+ let(:restored_project_json) { project_tree_restorer.restore }
+
+ before do
+ allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
+ end
+
+ context 'JSON' do
+ it 'restores models based on JSON' do
+ expect(restored_project_json).to be true
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
new file mode 100644
index 00000000000..8d29b2f8fd1
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -0,0 +1,149 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
+ describe 'saves the project tree into a json object' do
+
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:project_tree_saver) { described_class.new(project: project, shared: shared) }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:user) { create(:user) }
+ let(:project) { setup_project }
+
+ before do
+ project.team << [user, :master]
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'saves project successfully' do
+ expect(project_tree_saver.save).to be true
+ end
+
+ context 'JSON' do
+
+ let(:saved_project_json) do
+ project_tree_saver.save
+ project_json(project_tree_saver.full_path)
+ end
+
+ it 'saves the correct json' do
+ expect(saved_project_json).to include({ "visibility_level" => 20 })
+ end
+
+ it 'has events' do
+ expect(saved_project_json['events']).not_to be_empty
+ end
+
+ it 'has milestones' do
+ expect(saved_project_json['milestones']).not_to be_empty
+ end
+
+ it 'has merge requests' do
+ expect(saved_project_json['merge_requests']).not_to be_empty
+ end
+
+ it 'has labels' do
+ expect(saved_project_json['labels']).not_to be_empty
+ end
+
+ it 'has snippets' do
+ expect(saved_project_json['snippets']).not_to be_empty
+ end
+
+ it 'has snippet notes' do
+ expect(saved_project_json['snippets'].first['notes']).not_to be_empty
+ end
+
+ it 'has releases' do
+ expect(saved_project_json['releases']).not_to be_empty
+ end
+
+ it 'has issues' do
+ expect(saved_project_json['issues']).not_to be_empty
+ end
+
+ it 'has issue comments' do
+ expect(saved_project_json['issues'].first['notes']).not_to be_empty
+ end
+
+ it 'has author on issue comments' do
+ expect(saved_project_json['issues'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has project members' do
+ expect(saved_project_json['project_members']).not_to be_empty
+ end
+
+ it 'has merge requests diffs' do
+ expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty
+ end
+
+ it 'has merge requests comments' do
+ expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty
+ end
+
+ it 'has author on merge requests comments' do
+ expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty
+ end
+
+ it 'has pipeline statuses' do
+ expect(saved_project_json['pipelines'].first['statuses']).not_to be_empty
+ end
+
+ it 'has pipeline builds' do
+ expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1)
+ end
+
+ it 'has pipeline commits' do
+ expect(saved_project_json['pipelines']).not_to be_empty
+ end
+
+ it 'has ci pipeline notes' do
+ expect(saved_project_json['pipelines'].first['notes']).not_to be_empty
+ end
+ end
+ end
+
+ def setup_project
+ issue = create(:issue, assignee: user)
+ merge_request = create(:merge_request)
+ label = create(:label)
+ snippet = create(:project_snippet)
+ release = create(:release)
+
+ project = create(:project,
+ :public,
+ issues: [issue],
+ merge_requests: [merge_request],
+ labels: [label],
+ snippets: [snippet],
+ releases: [release]
+ )
+
+ commit_status = create(:commit_status, project: project)
+
+ ci_pipeline = create(:ci_pipeline,
+ project: project,
+ sha: merge_request.last_commit.id,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
+
+ create(:ci_build, pipeline: ci_pipeline, project: project)
+ create(:milestone, project: project)
+ create(:note, noteable: issue, project: project)
+ create(:note, noteable: merge_request, project: project)
+ create(:note, noteable: snippet, project: project)
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: ci_pipeline.sha)
+ project
+ end
+
+ def project_json(filename)
+ JSON.parse(IO.read(filename))
+ end
+end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
new file mode 100644
index 00000000000..109522fa626
--- /dev/null
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::Reader, lib: true do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path:'') }
+ let(:test_config) { 'spec/support/import_export/import_export.yml' }
+ let(:project_tree_hash) do
+ {
+ only: [:name, :path],
+ include: [:issues, :labels,
+ { merge_requests: {
+ only: [:id],
+ except: [:iid],
+ include: [:merge_request_diff, :merge_request_test]
+ } },
+ { commit_statuses: { include: :commit } }]
+ }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
+ end
+
+ it 'generates hash from project tree config' do
+ expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash)
+ end
+
+ context 'individual scenarios' do
+
+ it 'generates the correct hash for a single project relation' do
+ setup_yaml(project_tree: [:issues])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
+ end
+
+ it 'generates the correct hash for a multiple project relation' do
+ setup_yaml(project_tree: [:issues, :snippets])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [:issues, :snippets])
+ end
+
+ it 'generates the correct hash for a single sub-relation' do
+ setup_yaml(project_tree: [issues: [:notes]])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { include: :notes } }])
+ end
+
+ it 'generates the correct hash for a multiple sub-relation' do
+ setup_yaml(project_tree: [merge_requests: [:notes, :merge_request_diff]])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: [:notes, :merge_request_diff] } }])
+ end
+
+ it 'generates the correct hash for a sub-relation with another sub-relation' do
+ setup_yaml(project_tree: [merge_requests: [notes: :author]])
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: { notes: { include: :author } } } }])
+ end
+
+ it 'generates the correct hash for a relation with included attributes' do
+ setup_yaml(project_tree: [:issues], included_attributes: { issues: [:name, :description] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { only: [:name, :description] } }])
+ end
+
+ it 'generates the correct hash for a relation with excluded attributes' do
+ setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name] } }])
+ end
+
+ it 'generates the correct hash for a relation with both excluded and included attributes' do
+ setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }, included_attributes: { issues: [:description] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name], only: [:description] } }])
+ end
+
+ it 'generates the correct hash for a relation with custom methods' do
+ setup_yaml(project_tree: [:issues], methods: { issues: [:name] })
+
+ expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
+ end
+
+ def setup_yaml(hash)
+ allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
new file mode 100644
index 00000000000..590a9a7e1a5
--- /dev/null
+++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RepoSaver, services: true do
+ describe 'bundle a project Git repo' do
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:bundler) { described_class.new(project: project, shared: shared) }
+
+ before do
+ project.team << [user, :master]
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'bundles the repo successfully' do
+ expect(bundler.save).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
new file mode 100644
index 00000000000..b9ffc8694a5
--- /dev/null
+++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::WikiRepoSaver, services: true do
+ describe 'bundle a wiki Git repo' do
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
+ let!(:project_wiki) { ProjectWiki.new(project, user) }
+
+ before do
+ project.team << [user, :master]
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ project_wiki.wiki
+ project_wiki.create_page("index", "test content")
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'bundles the repo successfully' do
+ expect(wiki_bundler.save).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index cdf641341cb..8809b7e3f12 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -78,9 +78,8 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
- expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, hash_including(:duration, :cpu_duration),
- method: 'Dummy.foo')
+ expect(transaction).to receive(:measure_method).
+ with('Dummy.foo')
@dummy.foo
end
@@ -158,9 +157,8 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
- expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, hash_including(:duration, :cpu_duration),
- method: 'Dummy#bar')
+ expect(transaction).to receive(:measure_method).
+ with('Dummy#bar')
@dummy.new.bar
end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
new file mode 100644
index 00000000000..8d05081eecb
--- /dev/null
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -0,0 +1,91 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::MethodCall do
+ let(:method_call) { described_class.new('Foo#bar', 'foo') }
+
+ describe '#measure' do
+ it 'measures the performance of the supplied block' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.real_time).to be_a_kind_of(Numeric)
+ expect(method_call.cpu_time).to be_a_kind_of(Numeric)
+ expect(method_call.call_count).to eq(1)
+ end
+ end
+
+ describe '#to_metric' do
+ it 'returns a Metric instance' do
+ method_call.measure { 'foo' }
+ metric = method_call.to_metric
+
+ expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
+ expect(metric.series).to eq('foo')
+
+ expect(metric.values[:duration]).to be_a_kind_of(Numeric)
+ expect(metric.values[:cpu_duration]).to be_a_kind_of(Numeric)
+ expect(metric.values[:call_count]).to an_instance_of(Fixnum)
+
+ expect(metric.tags).to eq({ method: 'Foo#bar' })
+ end
+ end
+
+ describe '#above_threshold?' do
+ it 'returns false when the total call time is not above the threshold' do
+ expect(method_call.above_threshold?).to eq(false)
+ end
+
+ it 'returns true when the total call time is above the threshold' do
+ expect(method_call).to receive(:real_time).and_return(9000)
+
+ expect(method_call.above_threshold?).to eq(true)
+ end
+ end
+
+ describe '#call_count' do
+ context 'without any method calls' do
+ it 'returns 0' do
+ expect(method_call.call_count).to eq(0)
+ end
+ end
+
+ context 'with method calls' do
+ it 'returns the number of method calls' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.call_count).to eq(1)
+ end
+ end
+ end
+
+ describe '#cpu_time' do
+ context 'without timings' do
+ it 'returns 0.0' do
+ expect(method_call.cpu_time).to eq(0.0)
+ end
+ end
+
+ context 'with timings' do
+ it 'returns the total CPU time' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.cpu_time >= 0.0).to be(true)
+ end
+ end
+ end
+
+ describe '#real_time' do
+ context 'without timings' do
+ it 'returns 0.0' do
+ expect(method_call.real_time).to eq(0.0)
+ end
+ end
+
+ context 'with timings' do
+ it 'returns the total real time' do
+ method_call.measure { 'foo' }
+
+ expect(method_call.real_time >= 0.0).to be(true)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index 40289f8b972..f264ed64029 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -58,6 +58,22 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction.values[:request_method]).to eq('GET')
expect(transaction.values[:request_uri]).to eq('/foo')
end
+
+ context "when URI includes sensitive parameters" do
+ let(:env) do
+ {
+ 'REQUEST_METHOD' => 'GET',
+ 'REQUEST_URI' => '/foo?private_token=my-token',
+ 'PATH_INFO' => '/foo',
+ 'QUERY_STRING' => 'private_token=my_token',
+ 'action_dispatch.parameter_filter' => [:private_token]
+ }
+ end
+
+ it 'stores the request URI with the sensitive parameters filtered' do
+ expect(transaction.values[:request_uri]).to eq('/foo?private_token=[FILTERED]')
+ end
+ end
end
describe '#tag_controller' do
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 1d5a51a157e..3b1c67a2147 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -46,6 +46,22 @@ describe Gitlab::Metrics::Transaction do
end
end
+ describe '#measure_method' do
+ it 'adds a new method if it does not exist already' do
+ transaction.measure_method('Foo#bar') { 'foo' }
+
+ expect(transaction.methods['Foo#bar']).
+ to be_an_instance_of(Gitlab::Metrics::MethodCall)
+ end
+
+ it 'adds timings to an existing method call' do
+ transaction.measure_method('Foo#bar') { 'foo' }
+ transaction.measure_method('Foo#bar') { 'foo' }
+
+ expect(transaction.methods['Foo#bar'].call_count).to eq(2)
+ end
+ end
+
describe '#increment' do
it 'increments a counter' do
transaction.increment(:time, 1)
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7c617723e6d..7b4ccc83915 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -105,7 +105,8 @@ describe Gitlab::ReferenceExtractor, lib: true do
it 'returns JIRA issues for a JIRA-integrated project' do
subject.analyze('JIRA-123 and FOOBAR-4567')
- expect(subject.issues).to eq [JiraIssue.new('JIRA-123', project), JiraIssue.new('FOOBAR-4567', project)]
+ expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('FOOBAR-4567', project)]
end
end
diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb
index dc3062a4332..d6588efc486 100644
--- a/spec/mailers/previews/devise_mailer_preview.rb
+++ b/spec/mailers/previews/devise_mailer_preview.rb
@@ -1,11 +1,30 @@
class DeviseMailerPreview < ActionMailer::Preview
def confirmation_instructions_for_signup
- user = User.new(name: 'Jane Doe', email: 'signup@example.com')
- DeviseMailer.confirmation_instructions(user, 'faketoken', {})
+ DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
end
def confirmation_instructions_for_new_email
user = User.last
+ user.unconfirmed_email = 'unconfirmed@example.com'
+
DeviseMailer.confirmation_instructions(user, 'faketoken', {})
end
+
+ def reset_password_instructions
+ DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def unlock_instructions
+ DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {})
+ end
+
+ def password_change
+ DeviseMailer.password_change(unsaved_user, {})
+ end
+
+ private
+
+ def unsaved_user
+ User.new(name: 'Jane Doe', email: 'jdoe@example.com')
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 0d769ed7324..34507cf5083 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -258,6 +258,19 @@ describe Ci::Pipeline, models: true do
end
end
end
+
+ context 'when no builds created' do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
+ end
+
+ it 'returns false' do
+ expect(pipeline.create_builds(nil)).to be_falsey
+ expect(pipeline).not_to be_persisted
+ end
+ end
end
describe "#finished_at" do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ccdcb29f773..2c19aa3f67f 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -158,6 +158,18 @@ describe Group, models: true do
it { expect(group.has_master?(@members[:requester])).to be_falsey }
end
+ describe '#owners' do
+ let(:owner) { create(:user) }
+ let(:developer) { create(:user) }
+
+ it 'returns the owners of a Group' do
+ group.add_owner(owner)
+ group.add_developer(developer)
+
+ expect(group.owners).to eq([owner])
+ end
+ end
+
def setup_group_members(group)
members = {
owner: create(:user),
diff --git a/spec/models/jira_issue_spec.rb b/spec/models/jira_issue_spec.rb
deleted file mode 100644
index 1634265b439..00000000000
--- a/spec/models/jira_issue_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require 'spec_helper'
-
-describe JiraIssue do
- let(:project) { create(:project) }
- subject { JiraIssue.new('JIRA-123', project) }
-
- describe 'id' do
- subject { super().id }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe 'iid' do
- subject { super().iid }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe 'to_s' do
- subject { super().to_s }
- it { is_expected.to eq('JIRA-123') }
- end
-
- describe :== do
- specify { expect(subject).to eq(JiraIssue.new('JIRA-123', project)) }
- specify { expect(subject).not_to eq(JiraIssue.new('JIRA-124', project)) }
-
- it 'only compares with JiraIssues' do
- expect(subject).not_to eq('JIRA-123')
- end
- end
-end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
new file mode 100644
index 00000000000..46eb71cef14
--- /dev/null
+++ b/spec/models/personal_access_token_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe PersonalAccessToken, models: true do
+ describe ".generate" do
+ it "generates a random token" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token.token).to be_present
+ end
+
+ it "doesn't save the record" do
+ personal_access_token = PersonalAccessToken.generate({})
+ expect(personal_access_token).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 5309cfb99ff..c9517324541 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -76,7 +76,8 @@ describe JiraService, models: true do
end
it "should call JIRA API" do
- @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
+ @jira_service.execute(merge_request,
+ ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
@@ -84,7 +85,8 @@ describe JiraService, models: true do
it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
- @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
+ @jira_service.execute(merge_request,
+ ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @api_url).with(
body: /this-is-a-custom-id/
).once
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index fedab1f913b..53c8408633c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -220,7 +220,7 @@ describe Project, models: true do
end
end
- describe :find_with_namespace do
+ describe '.find_with_namespace' do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
@@ -231,6 +231,22 @@ describe Project, models: true do
it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
end
+
+ context 'when multiple projects using a similar name exist' do
+ let(:group) { create(:group, name: 'gitlab') }
+
+ let!(:project1) do
+ create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group)
+ end
+
+ let!(:project2) do
+ create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group)
+ end
+
+ it 'returns the row where the path matches literally' do
+ expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2)
+ end
+ end
end
describe :to_param do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 8c2347992f1..d8350000bf6 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -31,6 +31,47 @@ describe Repository, models: true do
it { is_expected.not_to include('v1.0.0') }
end
+ describe 'tags_sorted_by' do
+ context 'name' do
+ subject { repository.tags_sorted_by('name').map(&:name) }
+
+ it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
+ end
+
+ context 'updated' do
+ let(:tag_a) { repository.find_tag('v1.0.0') }
+ let(:tag_b) { repository.find_tag('v1.1.0') }
+
+ context 'desc' do
+ subject { repository.tags_sorted_by('updated_desc').map(&:name) }
+
+ before do
+ double_first = double(committed_date: Time.now)
+ double_last = double(committed_date: Time.now - 1.second)
+
+ allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first)
+ allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last)
+ end
+
+ it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
+ end
+
+ context 'asc' do
+ subject { repository.tags_sorted_by('updated_asc').map(&:name) }
+
+ before do
+ double_first = double(committed_date: Time.now - 1.second)
+ double_last = double(committed_date: Time.now)
+
+ allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last)
+ allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first)
+ end
+
+ it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
+ end
+ end
+ end
+
describe :last_commit_for_path do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 0c19094ec54..f22db61e744 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
-describe API, api: true do
+describe API::Helpers, api: true do
+
include API::Helpers
include ApiHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
@@ -39,24 +41,64 @@ describe API, api: true do
end
describe ".current_user" do
- it "should return nil for an invalid token" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
- expect(current_user).to be_nil
- end
-
- it "should return nil for a user without access" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
- expect(current_user).to be_nil
+ describe "when authenticating using a user's private token" do
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
+ expect(current_user).to eq(user)
+ end
end
- it "should leave user as is when sudo not specified" do
- env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- expect(current_user).to eq(user)
- clear_env
- params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
- expect(current_user).to eq(user)
+ describe "when authenticating using a user's personal access tokens" do
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ it "should return nil for an invalid token" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it "should return nil for a user without access" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ expect(current_user).to be_nil
+ end
+
+ it "should leave user as is when sudo not specified" do
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ expect(current_user).to eq(user)
+ clear_env
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+ expect(current_user).to eq(user)
+ end
+
+ it 'does not allow revoked tokens' do
+ personal_access_token.revoke!
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
+
+ it 'does not allow expired tokens' do
+ personal_access_token.update_attributes!(expires_at: 1.day.ago)
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ expect(current_user).to be_nil
+ end
end
it "should change current user to sudo when admin" do
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
new file mode 100644
index 00000000000..41cbf0c6669
--- /dev/null
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe API::SidekiqMetrics, api: true do
+ include ApiHelpers
+
+ let(:admin) { create(:user, :admin) }
+
+ describe 'GET sidekiq/*' do
+ it 'defines the `queue_metrics` endpoint' do
+ get api('/sidekiq/queue_metrics', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a Hash
+ end
+
+ it 'defines the `process_metrics` endpoint' do
+ get api('/sidekiq/process_metrics', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response['processes']).to be_an Array
+ end
+
+ it 'defines the `job_stats` endpoint' do
+ get api('/sidekiq/job_stats', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a Hash
+ end
+
+ it 'defines the `compound_metrics` endpoint' do
+ get api('/sidekiq/compound_metrics', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a Hash
+ expect(json_response['queues']).to be_a Hash
+ expect(json_response['processes']).to be_an Array
+ expect(json_response['jobs']).to be_a Hash
+ end
+ end
+end
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
index 984b78487d4..8b0becd83d3 100644
--- a/spec/services/ci/create_builds_service_spec.rb
+++ b/spec/services/ci/create_builds_service_spec.rb
@@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do
#
subject do
- described_class.new(pipeline).execute('test', nil, user, status)
+ described_class.new(pipeline).execute('test', user, status, nil)
end
context 'next builds available' do
@@ -17,6 +17,10 @@ describe Ci::CreateBuildsService, services: true do
it { is_expected.to be_an_instance_of Array }
it { is_expected.to all(be_an_instance_of Ci::Build) }
+
+ it 'does not persist created builds' do
+ expect(subject.first).not_to be_persisted
+ end
end
context 'builds skipped' do
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index d91fc574299..f28f2f1438d 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -45,11 +45,73 @@ module Ci
end
end
+ context 'deleted projects' do
+ before do
+ project.update(pending_delete: true)
+ end
+
+ context 'for shared runners' do
+ before do
+ project.update(shared_runners_enabled: true)
+ end
+
+ it 'does not pick a build' do
+ expect(service.execute(shared_runner)).to be_nil
+ end
+ end
+
+ context 'for specific runner' do
+ it 'does not pick a build' do
+ expect(service.execute(specific_runner)).to be_nil
+ end
+ end
+ end
+
context 'allow shared runners' do
before do
project.update(shared_runners_enabled: true)
end
+ context 'for multiple builds' do
+ let!(:project2) { create :empty_project, shared_runners_enabled: true }
+ let!(:pipeline2) { create :ci_pipeline, project: project2 }
+ let!(:project3) { create :empty_project, shared_runners_enabled: true }
+ let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:build1_project1) { pending_build }
+ let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
+ let!(:build2_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
+ let!(:build1_project3) { FactoryGirl.create :ci_build, pipeline: pipeline3 }
+
+ it 'prefers projects without builds first' do
+ # it gets for one build from each of the projects
+ expect(service.execute(shared_runner)).to eq(build1_project1)
+ expect(service.execute(shared_runner)).to eq(build1_project2)
+ expect(service.execute(shared_runner)).to eq(build1_project3)
+
+ # then it gets a second build from each of the projects
+ expect(service.execute(shared_runner)).to eq(build2_project1)
+ expect(service.execute(shared_runner)).to eq(build2_project2)
+
+ # in the end the third build
+ expect(service.execute(shared_runner)).to eq(build3_project1)
+ end
+
+ it 'equalises number of running builds' do
+ # after finishing the first build for project 1, get a second build from the same project
+ expect(service.execute(shared_runner)).to eq(build1_project1)
+ build1_project1.success
+ expect(service.execute(shared_runner)).to eq(build2_project1)
+
+ expect(service.execute(shared_runner)).to eq(build1_project2)
+ build1_project2.success
+ expect(service.execute(shared_runner)).to eq(build2_project2)
+ expect(service.execute(shared_runner)).to eq(build1_project3)
+ expect(service.execute(shared_runner)).to eq(build3_project1)
+ end
+ end
+
context 'shared runner' do
let(:build) { service.execute(shared_runner) }
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
index a5b4d9f05de..deab242f45a 100644
--- a/spec/services/create_commit_builds_service_spec.rb
+++ b/spec/services/create_commit_builds_service_spec.rb
@@ -39,7 +39,7 @@ describe CreateCommitBuildsService, services: true do
end
it "creates commit if there is no appropriate job but deploy job has right ref setting" do
- config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
+ config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
stub_ci_pipeline_yaml_file(config)
result = service.execute(project, user,
@@ -81,7 +81,7 @@ describe CreateCommitBuildsService, services: true do
expect(pipeline.yaml_errors).not_to be_nil
end
- describe :ci_skip? do
+ context 'when commit contains a [ci skip] directive' do
let(:message) { "some message[ci skip]" }
before do
@@ -171,5 +171,24 @@ describe CreateCommitBuildsService, services: true do
expect(pipeline.status).to eq("failed")
expect(pipeline.builds.any?).to be false
end
+
+ context 'when there are no jobs for this pipeline' do
+ before do
+ config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not create a new pipeline' do
+ result = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_falsey
+ expect(Ci::Build.all).to be_empty
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+ end
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 18692f1279a..f99ad046f0d 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -312,7 +312,8 @@ describe GitPushService, services: true do
end
it "doesn't close issues when external issue tracker is in use" do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ allow_any_instance_of(Project).to receive(:default_issues_tracker?).
+ and_return(false)
# The push still shouldn't create cross-reference notes.
expect do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 09f0ee3871d..85dd30bf48c 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -529,7 +529,7 @@ describe SystemNoteService, services: true do
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
- let(:jira_issue) { JiraIssue.new("JIRA-1", project)}
+ let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
let(:commit) { project.commit }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 26f09cdbaf9..b4522536724 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -108,17 +108,25 @@ describe TodoService, services: true do
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
- it 'does not create todo when when tasks are marked as completed' do
- issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
+ context 'issues with a task list' do
+ it 'does not create todo when tasks are marked as completed' do
+ issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
+
+ service.update_issue(issue, author)
+
+ should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
+ end
- service.update_issue(issue, author)
+ it 'does not raise an error when description not change' do
+ issue.update(title: 'Sample')
- should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
+ expect { service.update_issue(issue, author) }.not_to raise_error
+ end
end
end
@@ -165,6 +173,48 @@ describe TodoService, services: true do
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
end
+
+ describe 'cached counts' do
+ it 'updates when todos change' do
+ create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ expect(john_doe.todos_done_count).to eq(0)
+ expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.mark_pending_todos_as_done(issue, john_doe)
+
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(0)
+ end
+ end
+ end
+
+ describe '#mark_todos_as_done' do
+ it 'marks related todos for the user as done' do
+ first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ service.mark_todos_as_done([first_todo, second_todo], john_doe)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ end
+
+ describe 'cached counts' do
+ it 'updates when todos change' do
+ todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+
+ expect(john_doe.todos_done_count).to eq(0)
+ expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.mark_todos_as_done([todo], john_doe)
+
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(0)
+ end
+ end
end
describe '#new_note' do
@@ -285,17 +335,25 @@ describe TodoService, services: true do
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
- it 'does not create todo when when tasks are marked as completed' do
- mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
+ context 'with a task list' do
+ it 'does not create todo when tasks are marked as completed' do
+ mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
- service.update_merge_request(mr_assigned, author)
+ service.update_merge_request(mr_assigned, author)
- should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'does not raise an error when description not change' do
+ mr_assigned.update(title: 'Sample')
+
+ expect { service.update_merge_request(mr_assigned, author) }.not_to raise_error
+ end
end
end
@@ -379,6 +437,18 @@ describe TodoService, services: true do
end
end
+ it 'updates cached counts when a todo is created' do
+ issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
+
+ expect(john_doe.todos_pending_count).to eq(0)
+ expect(john_doe).to receive(:update_todos_count_cache)
+
+ service.new_issue(issue, author)
+
+ expect(Todo.where(user_id: john_doe.id, state: :pending).count).to eq 1
+ expect(john_doe.todos_pending_count).to eq(1)
+ end
+
def should_create_todo(attributes = {})
attributes.reverse_merge!(
project: project,
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
new file mode 100644
index 00000000000..3ceec506401
--- /dev/null
+++ b/spec/support/import_export/import_export.yml
@@ -0,0 +1,20 @@
+# Class relationships to be included in the project import/export
+project_tree:
+ - :issues
+ - :labels
+ - merge_requests:
+ - :merge_request_diff
+ - :merge_request_test
+ - commit_statuses:
+ - :commit
+
+included_attributes:
+ project:
+ - :name
+ - :path
+ merge_requests:
+ - :id
+
+excluded_attributes:
+ merge_requests:
+ - :iid \ No newline at end of file
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index e3827cae9a6..7d6668920c0 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -20,6 +20,10 @@ describe ExpireBuildArtifactsWorker do
it 'does remove files' do
expect(build.reload.artifacts_file.exists?).to be_falsey
end
+
+ it 'does nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).to be_nil
+ end
end
context 'with not yet expired artifacts' do
@@ -32,6 +36,10 @@ describe ExpireBuildArtifactsWorker do
it 'does not remove files' do
expect(build.reload.artifacts_file.exists?).to be_truthy
end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
end
context 'without expire date' do
@@ -44,6 +52,10 @@ describe ExpireBuildArtifactsWorker do
it 'does not remove files' do
expect(build.reload.artifacts_file.exists?).to be_truthy
end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
end
context 'for expired artifacts' do
diff --git a/vendor/assets/javascripts/raphael.js b/vendor/assets/javascripts/raphael.js
new file mode 100644
index 00000000000..3f3f8a0b7f6
--- /dev/null
+++ b/vendor/assets/javascripts/raphael.js
@@ -0,0 +1,8239 @@
+// ┌────────────────────────────────────────────────────────────────────┐ \\
+// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\
+// ├────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\
+// ├────────────────────────────────────────────────────────────────────┤ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\
+// └────────────────────────────────────────────────────────────────────┘ \\
+// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ┌────────────────────────────────────────────────────────────┐ \\
+// │ Eve 0.4.2 - JavaScript Events Library │ \\
+// ├────────────────────────────────────────────────────────────┤ \\
+// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\
+// └────────────────────────────────────────────────────────────┘ \\
+
+(function (glob) {
+ var version = "0.4.2",
+ has = "hasOwnProperty",
+ separator = /[\.\/]/,
+ wildcard = "*",
+ fun = function () {},
+ numsort = function (a, b) {
+ return a - b;
+ },
+ current_event,
+ stop,
+ events = {n: {}},
+ /*\
+ * eve
+ [ method ]
+
+ * Fires event with given `name`, given scope and other parameters.
+
+ > Arguments
+
+ - name (string) name of the *event*, dot (`.`) or slash (`/`) separated
+ - scope (object) context for the event handlers
+ - varargs (...) the rest of arguments will be sent to event handlers
+
+ = (object) array of returned values from the listeners
+ \*/
+ eve = function (name, scope) {
+ name = String(name);
+ var e = events,
+ oldstop = stop,
+ args = Array.prototype.slice.call(arguments, 2),
+ listeners = eve.listeners(name),
+ z = 0,
+ f = false,
+ l,
+ indexed = [],
+ queue = {},
+ out = [],
+ ce = current_event,
+ errors = [];
+ current_event = name;
+ stop = 0;
+ for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) {
+ indexed.push(listeners[i].zIndex);
+ if (listeners[i].zIndex < 0) {
+ queue[listeners[i].zIndex] = listeners[i];
+ }
+ }
+ indexed.sort(numsort);
+ while (indexed[z] < 0) {
+ l = queue[indexed[z++]];
+ out.push(l.apply(scope, args));
+ if (stop) {
+ stop = oldstop;
+ return out;
+ }
+ }
+ for (i = 0; i < ii; i++) {
+ l = listeners[i];
+ if ("zIndex" in l) {
+ if (l.zIndex == indexed[z]) {
+ out.push(l.apply(scope, args));
+ if (stop) {
+ break;
+ }
+ do {
+ z++;
+ l = queue[indexed[z]];
+ l && out.push(l.apply(scope, args));
+ if (stop) {
+ break;
+ }
+ } while (l)
+ } else {
+ queue[l.zIndex] = l;
+ }
+ } else {
+ out.push(l.apply(scope, args));
+ if (stop) {
+ break;
+ }
+ }
+ }
+ stop = oldstop;
+ current_event = ce;
+ return out.length ? out : null;
+ };
+ // Undocumented. Debug only.
+ eve._events = events;
+ /*\
+ * eve.listeners
+ [ method ]
+
+ * Internal method which gives you array of all event handlers that will be triggered by the given `name`.
+
+ > Arguments
+
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated
+
+ = (array) array of event handlers
+ \*/
+ eve.listeners = function (name) {
+ var names = name.split(separator),
+ e = events,
+ item,
+ items,
+ k,
+ i,
+ ii,
+ j,
+ jj,
+ nes,
+ es = [e],
+ out = [];
+ for (i = 0, ii = names.length; i < ii; i++) {
+ nes = [];
+ for (j = 0, jj = es.length; j < jj; j++) {
+ e = es[j].n;
+ items = [e[names[i]], e[wildcard]];
+ k = 2;
+ while (k--) {
+ item = items[k];
+ if (item) {
+ nes.push(item);
+ out = out.concat(item.f || []);
+ }
+ }
+ }
+ es = nes;
+ }
+ return out;
+ };
+
+ /*\
+ * eve.on
+ [ method ]
+ **
+ * Binds given event handler with a given name. You can use wildcards “`*`” for the names:
+ | eve.on("*.under.*", f);
+ | eve("mouse.under.floor"); // triggers f
+ * Use @eve to trigger the listener.
+ **
+ > Arguments
+ **
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
+ - f (function) event handler function
+ **
+ = (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment.
+ > Example:
+ | eve.on("mouse", eatIt)(2);
+ | eve.on("mouse", scream);
+ | eve.on("mouse", catchIt)(1);
+ * This will ensure that `catchIt()` function will be called before `eatIt()`.
+ *
+ * If you want to put your handler before non-indexed handlers, specify a negative value.
+ * Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in case”.
+ \*/
+ eve.on = function (name, f) {
+ name = String(name);
+ if (typeof f != "function") {
+ return function () {};
+ }
+ var names = name.split(separator),
+ e = events;
+ for (var i = 0, ii = names.length; i < ii; i++) {
+ e = e.n;
+ e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}});
+ }
+ e.f = e.f || [];
+ for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) {
+ return fun;
+ }
+ e.f.push(f);
+ return function (zIndex) {
+ if (+zIndex == +zIndex) {
+ f.zIndex = +zIndex;
+ }
+ };
+ };
+ /*\
+ * eve.f
+ [ method ]
+ **
+ * Returns function that will fire given event with optional arguments.
+ * Arguments that will be passed to the result function will be also
+ * concated to the list of final arguments.
+ | el.onclick = eve.f("click", 1, 2);
+ | eve.on("click", function (a, b, c) {
+ | console.log(a, b, c); // 1, 2, [event object]
+ | });
+ > Arguments
+ - event (string) event name
+ - varargs (…) and any other arguments
+ = (function) possible event handler function
+ \*/
+ eve.f = function (event) {
+ var attrs = [].slice.call(arguments, 1);
+ return function () {
+ eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0)));
+ };
+ };
+ /*\
+ * eve.stop
+ [ method ]
+ **
+ * Is used inside an event handler to stop the event, preventing any subsequent listeners from firing.
+ \*/
+ eve.stop = function () {
+ stop = 1;
+ };
+ /*\
+ * eve.nt
+ [ method ]
+ **
+ * Could be used inside event handler to figure out actual name of the event.
+ **
+ > Arguments
+ **
+ - subname (string) #optional subname of the event
+ **
+ = (string) name of the event, if `subname` is not specified
+ * or
+ = (boolean) `true`, if current event’s name contains `subname`
+ \*/
+ eve.nt = function (subname) {
+ if (subname) {
+ return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(current_event);
+ }
+ return current_event;
+ };
+ /*\
+ * eve.nts
+ [ method ]
+ **
+ * Could be used inside event handler to figure out actual name of the event.
+ **
+ **
+ = (array) names of the event
+ \*/
+ eve.nts = function () {
+ return current_event.split(separator);
+ };
+ /*\
+ * eve.off
+ [ method ]
+ **
+ * Removes given function from the list of event listeners assigned to given name.
+ * If no arguments specified all the events will be cleared.
+ **
+ > Arguments
+ **
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
+ - f (function) event handler function
+ \*/
+ /*\
+ * eve.unbind
+ [ method ]
+ **
+ * See @eve.off
+ \*/
+ eve.off = eve.unbind = function (name, f) {
+ if (!name) {
+ eve._events = events = {n: {}};
+ return;
+ }
+ var names = name.split(separator),
+ e,
+ key,
+ splice,
+ i, ii, j, jj,
+ cur = [events];
+ for (i = 0, ii = names.length; i < ii; i++) {
+ for (j = 0; j < cur.length; j += splice.length - 2) {
+ splice = [j, 1];
+ e = cur[j].n;
+ if (names[i] != wildcard) {
+ if (e[names[i]]) {
+ splice.push(e[names[i]]);
+ }
+ } else {
+ for (key in e) if (e[has](key)) {
+ splice.push(e[key]);
+ }
+ }
+ cur.splice.apply(cur, splice);
+ }
+ }
+ for (i = 0, ii = cur.length; i < ii; i++) {
+ e = cur[i];
+ while (e.n) {
+ if (f) {
+ if (e.f) {
+ for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) {
+ e.f.splice(j, 1);
+ break;
+ }
+ !e.f.length && delete e.f;
+ }
+ for (key in e.n) if (e.n[has](key) && e.n[key].f) {
+ var funcs = e.n[key].f;
+ for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) {
+ funcs.splice(j, 1);
+ break;
+ }
+ !funcs.length && delete e.n[key].f;
+ }
+ } else {
+ delete e.f;
+ for (key in e.n) if (e.n[has](key) && e.n[key].f) {
+ delete e.n[key].f;
+ }
+ }
+ e = e.n;
+ }
+ }
+ };
+ /*\
+ * eve.once
+ [ method ]
+ **
+ * Binds given event handler with a given name to only run once then unbind itself.
+ | eve.once("login", f);
+ | eve("login"); // triggers f
+ | eve("login"); // no listeners
+ * Use @eve to trigger the listener.
+ **
+ > Arguments
+ **
+ - name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards
+ - f (function) event handler function
+ **
+ = (function) same return function as @eve.on
+ \*/
+ eve.once = function (name, f) {
+ var f2 = function () {
+ eve.unbind(name, f2);
+ return f.apply(this, arguments);
+ };
+ return eve.on(name, f2);
+ };
+ /*\
+ * eve.version
+ [ property (string) ]
+ **
+ * Current version of the library.
+ \*/
+ eve.version = version;
+ eve.toString = function () {
+ return "You are running Eve " + version;
+ };
+ (typeof module != "undefined" && module.exports) ? (module.exports = eve) : (typeof define != "undefined" ? (define("eve", [], function() { return eve; })) : (glob.eve = eve));
+})(window || this);
+// ┌─────────────────────────────────────────────────────────────────────┐ \\
+// │ "Raphaël 2.1.2" - JavaScript Vector Library │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
+// └─────────────────────────────────────────────────────────────────────┘ \\
+
+(function (glob, factory) {
+ // AMD support
+ if (typeof define === "function" && define.amd) {
+ // Define as an anonymous module
+ define(["eve"], function( eve ) {
+ return factory(glob, eve);
+ });
+ } else {
+ // Browser globals (glob is window)
+ // Raphael adds itself to window
+ factory(glob, glob.eve || (typeof require == "function" && require('eve')) );
+ }
+}(this, function (window, eve) {
+ /*\
+ * Raphael
+ [ method ]
+ **
+ * Creates a canvas object on which to draw.
+ * You must do this first, as all future calls to drawing methods
+ * from this instance will be bound to this canvas.
+ > Parameters
+ **
+ - container (HTMLElement|string) DOM element or its ID which is going to be a parent for drawing surface
+ - width (number)
+ - height (number)
+ - callback (function) #optional callback function which is going to be executed in the context of newly created paper
+ * or
+ - x (number)
+ - y (number)
+ - width (number)
+ - height (number)
+ - callback (function) #optional callback function which is going to be executed in the context of newly created paper
+ * or
+ - all (array) (first 3 or 4 elements in the array are equal to [containerID, width, height] or [x, y, width, height]. The rest are element descriptions in format {type: type, <attributes>}). See @Paper.add.
+ - callback (function) #optional callback function which is going to be executed in the context of newly created paper
+ * or
+ - onReadyCallback (function) function that is going to be called on DOM ready event. You can also subscribe to this event via Eve’s “DOMLoad” event. In this case method returns `undefined`.
+ = (object) @Paper
+ > Usage
+ | // Each of the following examples create a canvas
+ | // that is 320px wide by 200px high.
+ | // Canvas is created at the viewport’s 10,50 coordinate.
+ | var paper = Raphael(10, 50, 320, 200);
+ | // Canvas is created at the top left corner of the #notepad element
+ | // (or its top right corner in dir="rtl" elements)
+ | var paper = Raphael(document.getElementById("notepad"), 320, 200);
+ | // Same as above
+ | var paper = Raphael("notepad", 320, 200);
+ | // Image dump
+ | var set = Raphael(["notepad", 320, 200, {
+ | type: "rect",
+ | x: 10,
+ | y: 10,
+ | width: 25,
+ | height: 25,
+ | stroke: "#f00"
+ | }, {
+ | type: "text",
+ | x: 30,
+ | y: 40,
+ | text: "Dump"
+ | }]);
+ \*/
+ function R(first) {
+ if (R.is(first, "function")) {
+ return loaded ? first() : eve.on("raphael.DOMload", first);
+ } else if (R.is(first, array)) {
+ return R._engine.create[apply](R, first.splice(0, 3 + R.is(first[0], nu))).add(first);
+ } else {
+ var args = Array.prototype.slice.call(arguments, 0);
+ if (R.is(args[args.length - 1], "function")) {
+ var f = args.pop();
+ return loaded ? f.call(R._engine.create[apply](R, args)) : eve.on("raphael.DOMload", function () {
+ f.call(R._engine.create[apply](R, args));
+ });
+ } else {
+ return R._engine.create[apply](R, arguments);
+ }
+ }
+ }
+ R.version = "2.1.2";
+ R.eve = eve;
+ var loaded,
+ separator = /[, ]+/,
+ elements = {circle: 1, rect: 1, path: 1, ellipse: 1, text: 1, image: 1},
+ formatrg = /\{(\d+)\}/g,
+ proto = "prototype",
+ has = "hasOwnProperty",
+ g = {
+ doc: document,
+ win: window
+ },
+ oldRaphael = {
+ was: Object.prototype[has].call(g.win, "Raphael"),
+ is: g.win.Raphael
+ },
+ Paper = function () {
+ /*\
+ * Paper.ca
+ [ property (object) ]
+ **
+ * Shortcut for @Paper.customAttributes
+ \*/
+ /*\
+ * Paper.customAttributes
+ [ property (object) ]
+ **
+ * If you have a set of attributes that you would like to represent
+ * as a function of some number you can do it easily with custom attributes:
+ > Usage
+ | paper.customAttributes.hue = function (num) {
+ | num = num % 1;
+ | return {fill: "hsb(" + num + ", 0.75, 1)"};
+ | };
+ | // Custom attribute “hue” will change fill
+ | // to be given hue with fixed saturation and brightness.
+ | // Now you can use it like this:
+ | var c = paper.circle(10, 10, 10).attr({hue: .45});
+ | // or even like this:
+ | c.animate({hue: 1}, 1e3);
+ |
+ | // You could also create custom attribute
+ | // with multiple parameters:
+ | paper.customAttributes.hsb = function (h, s, b) {
+ | return {fill: "hsb(" + [h, s, b].join(",") + ")"};
+ | };
+ | c.attr({hsb: "0.5 .8 1"});
+ | c.animate({hsb: [1, 0, 0.5]}, 1e3);
+ \*/
+ this.ca = this.customAttributes = {};
+ },
+ paperproto,
+ appendChild = "appendChild",
+ apply = "apply",
+ concat = "concat",
+ supportsTouch = ('ontouchstart' in g.win) || g.win.DocumentTouch && g.doc instanceof DocumentTouch, //taken from Modernizr touch test
+ E = "",
+ S = " ",
+ Str = String,
+ split = "split",
+ events = "click dblclick mousedown mousemove mouseout mouseover mouseup touchstart touchmove touchend touchcancel"[split](S),
+ touchMap = {
+ mousedown: "touchstart",
+ mousemove: "touchmove",
+ mouseup: "touchend"
+ },
+ lowerCase = Str.prototype.toLowerCase,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ abs = math.abs,
+ pow = math.pow,
+ PI = math.PI,
+ nu = "number",
+ string = "string",
+ array = "array",
+ toString = "toString",
+ fillString = "fill",
+ objectToString = Object.prototype.toString,
+ paper = {},
+ push = "push",
+ ISURL = R._ISURL = /^url\(['"]?(.+?)['"]?\)$/i,
+ colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\))\s*$/i,
+ isnan = {"NaN": 1, "Infinity": 1, "-Infinity": 1},
+ bezierrg = /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/,
+ round = math.round,
+ setAttribute = "setAttribute",
+ toFloat = parseFloat,
+ toInt = parseInt,
+ upperCase = Str.prototype.toUpperCase,
+ availableAttrs = R._availableAttrs = {
+ "arrow-end": "none",
+ "arrow-start": "none",
+ blur: 0,
+ "clip-rect": "0 0 1e9 1e9",
+ cursor: "default",
+ cx: 0,
+ cy: 0,
+ fill: "#fff",
+ "fill-opacity": 1,
+ font: '10px "Arial"',
+ "font-family": '"Arial"',
+ "font-size": "10",
+ "font-style": "normal",
+ "font-weight": 400,
+ gradient: 0,
+ height: 0,
+ href: "http://raphaeljs.com/",
+ "letter-spacing": 0,
+ opacity: 1,
+ path: "M0,0",
+ r: 0,
+ rx: 0,
+ ry: 0,
+ src: "",
+ stroke: "#000",
+ "stroke-dasharray": "",
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "butt",
+ "stroke-miterlimit": 0,
+ "stroke-opacity": 1,
+ "stroke-width": 1,
+ target: "_blank",
+ "text-anchor": "middle",
+ title: "Raphael",
+ transform: "",
+ width: 0,
+ x: 0,
+ y: 0
+ },
+ availableAnimAttrs = R._availableAnimAttrs = {
+ blur: nu,
+ "clip-rect": "csv",
+ cx: nu,
+ cy: nu,
+ fill: "colour",
+ "fill-opacity": nu,
+ "font-size": nu,
+ height: nu,
+ opacity: nu,
+ path: "path",
+ r: nu,
+ rx: nu,
+ ry: nu,
+ stroke: "colour",
+ "stroke-opacity": nu,
+ "stroke-width": nu,
+ transform: "transform",
+ width: nu,
+ x: nu,
+ y: nu
+ },
+ whitespace = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]/g,
+ commaSpaces = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/,
+ hsrg = {hs: 1, rg: 1},
+ p2s = /,?([achlmqrstvxz]),?/gi,
+ pathCommand = /([achlmrqstvz])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
+ tCommand = /([rstm])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig,
+ pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/ig,
+ radial_gradient = R._radial_gradient = /^r(?:\(([^,]+?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*([^\)]+?)\))?/,
+ eldata = {},
+ sortByKey = function (a, b) {
+ return a.key - b.key;
+ },
+ sortByNumber = function (a, b) {
+ return toFloat(a) - toFloat(b);
+ },
+ fun = function () {},
+ pipe = function (x) {
+ return x;
+ },
+ rectPath = R._rectPath = function (x, y, w, h, r) {
+ if (r) {
+ return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
+ }
+ return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
+ },
+ ellipsePath = function (x, y, rx, ry) {
+ if (ry == null) {
+ ry = rx;
+ }
+ return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
+ },
+ getPath = R._getPath = {
+ path: function (el) {
+ return el.attr("path");
+ },
+ circle: function (el) {
+ var a = el.attrs;
+ return ellipsePath(a.cx, a.cy, a.r);
+ },
+ ellipse: function (el) {
+ var a = el.attrs;
+ return ellipsePath(a.cx, a.cy, a.rx, a.ry);
+ },
+ rect: function (el) {
+ var a = el.attrs;
+ return rectPath(a.x, a.y, a.width, a.height, a.r);
+ },
+ image: function (el) {
+ var a = el.attrs;
+ return rectPath(a.x, a.y, a.width, a.height);
+ },
+ text: function (el) {
+ var bbox = el._getBBox();
+ return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
+ },
+ set : function(el) {
+ var bbox = el._getBBox();
+ return rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
+ }
+ },
+ /*\
+ * Raphael.mapPath
+ [ method ]
+ **
+ * Transform the path string with given matrix.
+ > Parameters
+ - path (string) path string
+ - matrix (object) see @Matrix
+ = (string) transformed path string
+ \*/
+ mapPath = R.mapPath = function (path, matrix) {
+ if (!matrix) {
+ return path;
+ }
+ var x, y, i, j, ii, jj, pathi;
+ path = path2curve(path);
+ for (i = 0, ii = path.length; i < ii; i++) {
+ pathi = path[i];
+ for (j = 1, jj = pathi.length; j < jj; j += 2) {
+ x = matrix.x(pathi[j], pathi[j + 1]);
+ y = matrix.y(pathi[j], pathi[j + 1]);
+ pathi[j] = x;
+ pathi[j + 1] = y;
+ }
+ }
+ return path;
+ };
+
+ R._g = g;
+ /*\
+ * Raphael.type
+ [ property (string) ]
+ **
+ * Can be “SVG”, “VML” or empty, depending on browser support.
+ \*/
+ R.type = (g.win.SVGAngle || g.doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML");
+ if (R.type == "VML") {
+ var d = g.doc.createElement("div"),
+ b;
+ d.innerHTML = '<v:shape adj="1"/>';
+ b = d.firstChild;
+ b.style.behavior = "url(#default#VML)";
+ if (!(b && typeof b.adj == "object")) {
+ return (R.type = E);
+ }
+ d = null;
+ }
+ /*\
+ * Raphael.svg
+ [ property (boolean) ]
+ **
+ * `true` if browser supports SVG.
+ \*/
+ /*\
+ * Raphael.vml
+ [ property (boolean) ]
+ **
+ * `true` if browser supports VML.
+ \*/
+ R.svg = !(R.vml = R.type == "VML");
+ R._Paper = Paper;
+ /*\
+ * Raphael.fn
+ [ property (object) ]
+ **
+ * You can add your own method to the canvas. For example if you want to draw a pie chart,
+ * you can create your own pie chart function and ship it as a Raphaël plugin. To do this
+ * you need to extend the `Raphael.fn` object. You should modify the `fn` object before a
+ * Raphaël instance is created, otherwise it will take no effect. Please note that the
+ * ability for namespaced plugins was removed in Raphael 2.0. It is up to the plugin to
+ * ensure any namespacing ensures proper context.
+ > Usage
+ | Raphael.fn.arrow = function (x1, y1, x2, y2, size) {
+ | return this.path( ... );
+ | };
+ | // or create namespace
+ | Raphael.fn.mystuff = {
+ | arrow: function () {…},
+ | star: function () {…},
+ | // etc…
+ | };
+ | var paper = Raphael(10, 10, 630, 480);
+ | // then use it
+ | paper.arrow(10, 10, 30, 30, 5).attr({fill: "#f00"});
+ | paper.mystuff.arrow();
+ | paper.mystuff.star();
+ \*/
+ R.fn = paperproto = Paper.prototype = R.prototype;
+ R._id = 0;
+ R._oid = 0;
+ /*\
+ * Raphael.is
+ [ method ]
+ **
+ * Handful of replacements for `typeof` operator.
+ > Parameters
+ - o (…) any object or primitive
+ - type (string) name of the type, i.e. “string”, “function”, “number”, etc.
+ = (boolean) is given value is of given type
+ \*/
+ R.is = function (o, type) {
+ type = lowerCase.call(type);
+ if (type == "finite") {
+ return !isnan[has](+o);
+ }
+ if (type == "array") {
+ return o instanceof Array;
+ }
+ return (type == "null" && o === null) ||
+ (type == typeof o && o !== null) ||
+ (type == "object" && o === Object(o)) ||
+ (type == "array" && Array.isArray && Array.isArray(o)) ||
+ objectToString.call(o).slice(8, -1).toLowerCase() == type;
+ };
+
+ function clone(obj) {
+ if (typeof obj == "function" || Object(obj) !== obj) {
+ return obj;
+ }
+ var res = new obj.constructor;
+ for (var key in obj) if (obj[has](key)) {
+ res[key] = clone(obj[key]);
+ }
+ return res;
+ }
+
+ /*\
+ * Raphael.angle
+ [ method ]
+ **
+ * Returns angle between two or three points
+ > Parameters
+ - x1 (number) x coord of first point
+ - y1 (number) y coord of first point
+ - x2 (number) x coord of second point
+ - y2 (number) y coord of second point
+ - x3 (number) #optional x coord of third point
+ - y3 (number) #optional y coord of third point
+ = (number) angle in degrees.
+ \*/
+ R.angle = function (x1, y1, x2, y2, x3, y3) {
+ if (x3 == null) {
+ var x = x1 - x2,
+ y = y1 - y2;
+ if (!x && !y) {
+ return 0;
+ }
+ return (180 + math.atan2(-y, -x) * 180 / PI + 360) % 360;
+ } else {
+ return R.angle(x1, y1, x3, y3) - R.angle(x2, y2, x3, y3);
+ }
+ };
+ /*\
+ * Raphael.rad
+ [ method ]
+ **
+ * Transform angle to radians
+ > Parameters
+ - deg (number) angle in degrees
+ = (number) angle in radians.
+ \*/
+ R.rad = function (deg) {
+ return deg % 360 * PI / 180;
+ };
+ /*\
+ * Raphael.deg
+ [ method ]
+ **
+ * Transform angle to degrees
+ > Parameters
+ - rad (number) angle in radians
+ = (number) angle in degrees.
+ \*/
+ R.deg = function (rad) {
+ return Math.round ((rad * 180 / PI% 360)* 1000) / 1000;
+ };
+ /*\
+ * Raphael.snapTo
+ [ method ]
+ **
+ * Snaps given value to given grid.
+ > Parameters
+ - values (array|number) given array of values or step of the grid
+ - value (number) value to adjust
+ - tolerance (number) #optional tolerance for snapping. Default is `10`.
+ = (number) adjusted value.
+ \*/
+ R.snapTo = function (values, value, tolerance) {
+ tolerance = R.is(tolerance, "finite") ? tolerance : 10;
+ if (R.is(values, array)) {
+ var i = values.length;
+ while (i--) if (abs(values[i] - value) <= tolerance) {
+ return values[i];
+ }
+ } else {
+ values = +values;
+ var rem = value % values;
+ if (rem < tolerance) {
+ return value - rem;
+ }
+ if (rem > values - tolerance) {
+ return value - rem + values;
+ }
+ }
+ return value;
+ };
+
+ /*\
+ * Raphael.createUUID
+ [ method ]
+ **
+ * Returns RFC4122, version 4 ID
+ \*/
+ var createUUID = R.createUUID = (function (uuidRegEx, uuidReplacer) {
+ return function () {
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(uuidRegEx, uuidReplacer).toUpperCase();
+ };
+ })(/[xy]/g, function (c) {
+ var r = math.random() * 16 | 0,
+ v = c == "x" ? r : (r & 3 | 8);
+ return v.toString(16);
+ });
+
+ /*\
+ * Raphael.setWindow
+ [ method ]
+ **
+ * Used when you need to draw in `&lt;iframe>`. Switched window to the iframe one.
+ > Parameters
+ - newwin (window) new window object
+ \*/
+ R.setWindow = function (newwin) {
+ eve("raphael.setWindow", R, g.win, newwin);
+ g.win = newwin;
+ g.doc = g.win.document;
+ if (R._engine.initWin) {
+ R._engine.initWin(g.win);
+ }
+ };
+ var toHex = function (color) {
+ if (R.vml) {
+ // http://dean.edwards.name/weblog/2009/10/convert-any-colour-value-to-hex-in-msie/
+ var trim = /^\s+|\s+$/g;
+ var bod;
+ try {
+ var docum = new ActiveXObject("htmlfile");
+ docum.write("<body>");
+ docum.close();
+ bod = docum.body;
+ } catch(e) {
+ bod = createPopup().document.body;
+ }
+ var range = bod.createTextRange();
+ toHex = cacher(function (color) {
+ try {
+ bod.style.color = Str(color).replace(trim, E);
+ var value = range.queryCommandValue("ForeColor");
+ value = ((value & 255) << 16) | (value & 65280) | ((value & 16711680) >>> 16);
+ return "#" + ("000000" + value.toString(16)).slice(-6);
+ } catch(e) {
+ return "none";
+ }
+ });
+ } else {
+ var i = g.doc.createElement("i");
+ i.title = "Rapha\xebl Colour Picker";
+ i.style.display = "none";
+ g.doc.body.appendChild(i);
+ toHex = cacher(function (color) {
+ i.style.color = color;
+ return g.doc.defaultView.getComputedStyle(i, E).getPropertyValue("color");
+ });
+ }
+ return toHex(color);
+ },
+ hsbtoString = function () {
+ return "hsb(" + [this.h, this.s, this.b] + ")";
+ },
+ hsltoString = function () {
+ return "hsl(" + [this.h, this.s, this.l] + ")";
+ },
+ rgbtoString = function () {
+ return this.hex;
+ },
+ prepareRGB = function (r, g, b) {
+ if (g == null && R.is(r, "object") && "r" in r && "g" in r && "b" in r) {
+ b = r.b;
+ g = r.g;
+ r = r.r;
+ }
+ if (g == null && R.is(r, string)) {
+ var clr = R.getRGB(r);
+ r = clr.r;
+ g = clr.g;
+ b = clr.b;
+ }
+ if (r > 1 || g > 1 || b > 1) {
+ r /= 255;
+ g /= 255;
+ b /= 255;
+ }
+
+ return [r, g, b];
+ },
+ packageRGB = function (r, g, b, o) {
+ r *= 255;
+ g *= 255;
+ b *= 255;
+ var rgb = {
+ r: r,
+ g: g,
+ b: b,
+ hex: R.rgb(r, g, b),
+ toString: rgbtoString
+ };
+ R.is(o, "finite") && (rgb.opacity = o);
+ return rgb;
+ };
+
+ /*\
+ * Raphael.color
+ [ method ]
+ **
+ * Parses the color string and returns object with all values for the given color.
+ > Parameters
+ - clr (string) color string in one of the supported formats (see @Raphael.getRGB)
+ = (object) Combined RGB & HSB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue,
+ o hex (string) color in HTML/CSS format: #••••••,
+ o error (boolean) `true` if string can’t be parsed,
+ o h (number) hue,
+ o s (number) saturation,
+ o v (number) value (brightness),
+ o l (number) lightness
+ o }
+ \*/
+ R.color = function (clr) {
+ var rgb;
+ if (R.is(clr, "object") && "h" in clr && "s" in clr && "b" in clr) {
+ rgb = R.hsb2rgb(clr);
+ clr.r = rgb.r;
+ clr.g = rgb.g;
+ clr.b = rgb.b;
+ clr.hex = rgb.hex;
+ } else if (R.is(clr, "object") && "h" in clr && "s" in clr && "l" in clr) {
+ rgb = R.hsl2rgb(clr);
+ clr.r = rgb.r;
+ clr.g = rgb.g;
+ clr.b = rgb.b;
+ clr.hex = rgb.hex;
+ } else {
+ if (R.is(clr, "string")) {
+ clr = R.getRGB(clr);
+ }
+ if (R.is(clr, "object") && "r" in clr && "g" in clr && "b" in clr) {
+ rgb = R.rgb2hsl(clr);
+ clr.h = rgb.h;
+ clr.s = rgb.s;
+ clr.l = rgb.l;
+ rgb = R.rgb2hsb(clr);
+ clr.v = rgb.b;
+ } else {
+ clr = {hex: "none"};
+ clr.r = clr.g = clr.b = clr.h = clr.s = clr.v = clr.l = -1;
+ }
+ }
+ clr.toString = rgbtoString;
+ return clr;
+ };
+ /*\
+ * Raphael.hsb2rgb
+ [ method ]
+ **
+ * Converts HSB values to RGB object.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - v (number) value or brightness
+ = (object) RGB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue,
+ o hex (string) color in HTML/CSS format: #••••••
+ o }
+ \*/
+ R.hsb2rgb = function (h, s, v, o) {
+ if (this.is(h, "object") && "h" in h && "s" in h && "b" in h) {
+ v = h.b;
+ s = h.s;
+ o = h.o;
+ h = h.h;
+ }
+ h *= 360;
+ var R, G, B, X, C;
+ h = (h % 360) / 60;
+ C = v * s;
+ X = C * (1 - abs(h % 2 - 1));
+ R = G = B = v - C;
+
+ h = ~~h;
+ R += [C, X, 0, 0, X, C][h];
+ G += [X, C, C, X, 0, 0][h];
+ B += [0, 0, X, C, C, X][h];
+ return packageRGB(R, G, B, o);
+ };
+ /*\
+ * Raphael.hsl2rgb
+ [ method ]
+ **
+ * Converts HSL values to RGB object.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - l (number) luminosity
+ = (object) RGB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue,
+ o hex (string) color in HTML/CSS format: #••••••
+ o }
+ \*/
+ R.hsl2rgb = function (h, s, l, o) {
+ if (this.is(h, "object") && "h" in h && "s" in h && "l" in h) {
+ l = h.l;
+ s = h.s;
+ h = h.h;
+ }
+ if (h > 1 || s > 1 || l > 1) {
+ h /= 360;
+ s /= 100;
+ l /= 100;
+ }
+ h *= 360;
+ var R, G, B, X, C;
+ h = (h % 360) / 60;
+ C = 2 * s * (l < .5 ? l : 1 - l);
+ X = C * (1 - abs(h % 2 - 1));
+ R = G = B = l - C / 2;
+
+ h = ~~h;
+ R += [C, X, 0, 0, X, C][h];
+ G += [X, C, C, X, 0, 0][h];
+ B += [0, 0, X, C, C, X][h];
+ return packageRGB(R, G, B, o);
+ };
+ /*\
+ * Raphael.rgb2hsb
+ [ method ]
+ **
+ * Converts RGB values to HSB object.
+ > Parameters
+ - r (number) red
+ - g (number) green
+ - b (number) blue
+ = (object) HSB object in format:
+ o {
+ o h (number) hue
+ o s (number) saturation
+ o b (number) brightness
+ o }
+ \*/
+ R.rgb2hsb = function (r, g, b) {
+ b = prepareRGB(r, g, b);
+ r = b[0];
+ g = b[1];
+ b = b[2];
+
+ var H, S, V, C;
+ V = mmax(r, g, b);
+ C = V - mmin(r, g, b);
+ H = (C == 0 ? null :
+ V == r ? (g - b) / C :
+ V == g ? (b - r) / C + 2 :
+ (r - g) / C + 4
+ );
+ H = ((H + 360) % 6) * 60 / 360;
+ S = C == 0 ? 0 : C / V;
+ return {h: H, s: S, b: V, toString: hsbtoString};
+ };
+ /*\
+ * Raphael.rgb2hsl
+ [ method ]
+ **
+ * Converts RGB values to HSL object.
+ > Parameters
+ - r (number) red
+ - g (number) green
+ - b (number) blue
+ = (object) HSL object in format:
+ o {
+ o h (number) hue
+ o s (number) saturation
+ o l (number) luminosity
+ o }
+ \*/
+ R.rgb2hsl = function (r, g, b) {
+ b = prepareRGB(r, g, b);
+ r = b[0];
+ g = b[1];
+ b = b[2];
+
+ var H, S, L, M, m, C;
+ M = mmax(r, g, b);
+ m = mmin(r, g, b);
+ C = M - m;
+ H = (C == 0 ? null :
+ M == r ? (g - b) / C :
+ M == g ? (b - r) / C + 2 :
+ (r - g) / C + 4);
+ H = ((H + 360) % 6) * 60 / 360;
+ L = (M + m) / 2;
+ S = (C == 0 ? 0 :
+ L < .5 ? C / (2 * L) :
+ C / (2 - 2 * L));
+ return {h: H, s: S, l: L, toString: hsltoString};
+ };
+ R._path2string = function () {
+ return this.join(",").replace(p2s, "$1");
+ };
+ function repush(array, item) {
+ for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) {
+ return array.push(array.splice(i, 1)[0]);
+ }
+ }
+ function cacher(f, scope, postprocessor) {
+ function newf() {
+ var arg = Array.prototype.slice.call(arguments, 0),
+ args = arg.join("\u2400"),
+ cache = newf.cache = newf.cache || {},
+ count = newf.count = newf.count || [];
+ if (cache[has](args)) {
+ repush(count, args);
+ return postprocessor ? postprocessor(cache[args]) : cache[args];
+ }
+ count.length >= 1e3 && delete cache[count.shift()];
+ count.push(args);
+ cache[args] = f[apply](scope, arg);
+ return postprocessor ? postprocessor(cache[args]) : cache[args];
+ }
+ return newf;
+ }
+
+ var preload = R._preload = function (src, f) {
+ var img = g.doc.createElement("img");
+ img.style.cssText = "position:absolute;left:-9999em;top:-9999em";
+ img.onload = function () {
+ f.call(this);
+ this.onload = null;
+ g.doc.body.removeChild(this);
+ };
+ img.onerror = function () {
+ g.doc.body.removeChild(this);
+ };
+ g.doc.body.appendChild(img);
+ img.src = src;
+ };
+
+ function clrToString() {
+ return this.hex;
+ }
+
+ /*\
+ * Raphael.getRGB
+ [ method ]
+ **
+ * Parses colour string as RGB object
+ > Parameters
+ - colour (string) colour string in one of formats:
+ # <ul>
+ # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
+ # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
+ # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
+ # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
+ # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
+ # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
+ # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
+ # <li>hsl(•••, •••, •••) — same as hsb</li>
+ # <li>hsl(•••%, •••%, •••%) — same as hsb</li>
+ # </ul>
+ = (object) RGB object in format:
+ o {
+ o r (number) red,
+ o g (number) green,
+ o b (number) blue
+ o hex (string) color in HTML/CSS format: #••••••,
+ o error (boolean) true if string can’t be parsed
+ o }
+ \*/
+ R.getRGB = cacher(function (colour) {
+ if (!colour || !!((colour = Str(colour)).indexOf("-") + 1)) {
+ return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
+ }
+ if (colour == "none") {
+ return {r: -1, g: -1, b: -1, hex: "none", toString: clrToString};
+ }
+ !(hsrg[has](colour.toLowerCase().substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour));
+ var res,
+ red,
+ green,
+ blue,
+ opacity,
+ t,
+ values,
+ rgb = colour.match(colourRegExp);
+ if (rgb) {
+ if (rgb[2]) {
+ blue = toInt(rgb[2].substring(5), 16);
+ green = toInt(rgb[2].substring(3, 5), 16);
+ red = toInt(rgb[2].substring(1, 3), 16);
+ }
+ if (rgb[3]) {
+ blue = toInt((t = rgb[3].charAt(3)) + t, 16);
+ green = toInt((t = rgb[3].charAt(2)) + t, 16);
+ red = toInt((t = rgb[3].charAt(1)) + t, 16);
+ }
+ if (rgb[4]) {
+ values = rgb[4][split](commaSpaces);
+ red = toFloat(values[0]);
+ values[0].slice(-1) == "%" && (red *= 2.55);
+ green = toFloat(values[1]);
+ values[1].slice(-1) == "%" && (green *= 2.55);
+ blue = toFloat(values[2]);
+ values[2].slice(-1) == "%" && (blue *= 2.55);
+ rgb[1].toLowerCase().slice(0, 4) == "rgba" && (opacity = toFloat(values[3]));
+ values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
+ }
+ if (rgb[5]) {
+ values = rgb[5][split](commaSpaces);
+ red = toFloat(values[0]);
+ values[0].slice(-1) == "%" && (red *= 2.55);
+ green = toFloat(values[1]);
+ values[1].slice(-1) == "%" && (green *= 2.55);
+ blue = toFloat(values[2]);
+ values[2].slice(-1) == "%" && (blue *= 2.55);
+ (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
+ rgb[1].toLowerCase().slice(0, 4) == "hsba" && (opacity = toFloat(values[3]));
+ values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
+ return R.hsb2rgb(red, green, blue, opacity);
+ }
+ if (rgb[6]) {
+ values = rgb[6][split](commaSpaces);
+ red = toFloat(values[0]);
+ values[0].slice(-1) == "%" && (red *= 2.55);
+ green = toFloat(values[1]);
+ values[1].slice(-1) == "%" && (green *= 2.55);
+ blue = toFloat(values[2]);
+ values[2].slice(-1) == "%" && (blue *= 2.55);
+ (values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360);
+ rgb[1].toLowerCase().slice(0, 4) == "hsla" && (opacity = toFloat(values[3]));
+ values[3] && values[3].slice(-1) == "%" && (opacity /= 100);
+ return R.hsl2rgb(red, green, blue, opacity);
+ }
+ rgb = {r: red, g: green, b: blue, toString: clrToString};
+ rgb.hex = "#" + (16777216 | blue | (green << 8) | (red << 16)).toString(16).slice(1);
+ R.is(opacity, "finite") && (rgb.opacity = opacity);
+ return rgb;
+ }
+ return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString};
+ }, R);
+ /*\
+ * Raphael.hsb
+ [ method ]
+ **
+ * Converts HSB values to hex representation of the colour.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - b (number) value or brightness
+ = (string) hex representation of the colour.
+ \*/
+ R.hsb = cacher(function (h, s, b) {
+ return R.hsb2rgb(h, s, b).hex;
+ });
+ /*\
+ * Raphael.hsl
+ [ method ]
+ **
+ * Converts HSL values to hex representation of the colour.
+ > Parameters
+ - h (number) hue
+ - s (number) saturation
+ - l (number) luminosity
+ = (string) hex representation of the colour.
+ \*/
+ R.hsl = cacher(function (h, s, l) {
+ return R.hsl2rgb(h, s, l).hex;
+ });
+ /*\
+ * Raphael.rgb
+ [ method ]
+ **
+ * Converts RGB values to hex representation of the colour.
+ > Parameters
+ - r (number) red
+ - g (number) green
+ - b (number) blue
+ = (string) hex representation of the colour.
+ \*/
+ R.rgb = cacher(function (r, g, b) {
+ return "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1);
+ });
+ /*\
+ * Raphael.getColor
+ [ method ]
+ **
+ * On each call returns next colour in the spectrum. To reset it back to red call @Raphael.getColor.reset
+ > Parameters
+ - value (number) #optional brightness, default is `0.75`
+ = (string) hex representation of the colour.
+ \*/
+ R.getColor = function (value) {
+ var start = this.getColor.start = this.getColor.start || {h: 0, s: 1, b: value || .75},
+ rgb = this.hsb2rgb(start.h, start.s, start.b);
+ start.h += .075;
+ if (start.h > 1) {
+ start.h = 0;
+ start.s -= .2;
+ start.s <= 0 && (this.getColor.start = {h: 0, s: 1, b: start.b});
+ }
+ return rgb.hex;
+ };
+ /*\
+ * Raphael.getColor.reset
+ [ method ]
+ **
+ * Resets spectrum position for @Raphael.getColor back to red.
+ \*/
+ R.getColor.reset = function () {
+ delete this.start;
+ };
+
+ // http://schepers.cc/getting-to-the-point
+ function catmullRom2bezier(crp, z) {
+ var d = [];
+ for (var i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) {
+ var p = [
+ {x: +crp[i - 2], y: +crp[i - 1]},
+ {x: +crp[i], y: +crp[i + 1]},
+ {x: +crp[i + 2], y: +crp[i + 3]},
+ {x: +crp[i + 4], y: +crp[i + 5]}
+ ];
+ if (z) {
+ if (!i) {
+ p[0] = {x: +crp[iLen - 2], y: +crp[iLen - 1]};
+ } else if (iLen - 4 == i) {
+ p[3] = {x: +crp[0], y: +crp[1]};
+ } else if (iLen - 2 == i) {
+ p[2] = {x: +crp[0], y: +crp[1]};
+ p[3] = {x: +crp[2], y: +crp[3]};
+ }
+ } else {
+ if (iLen - 4 == i) {
+ p[3] = p[2];
+ } else if (!i) {
+ p[0] = {x: +crp[i], y: +crp[i + 1]};
+ }
+ }
+ d.push(["C",
+ (-p[0].x + 6 * p[1].x + p[2].x) / 6,
+ (-p[0].y + 6 * p[1].y + p[2].y) / 6,
+ (p[1].x + 6 * p[2].x - p[3].x) / 6,
+ (p[1].y + 6*p[2].y - p[3].y) / 6,
+ p[2].x,
+ p[2].y
+ ]);
+ }
+
+ return d;
+ }
+ /*\
+ * Raphael.parsePathString
+ [ method ]
+ **
+ * Utility method
+ **
+ * Parses given path string into an array of arrays of path segments.
+ > Parameters
+ - pathString (string|array) path string or array of segments (in the last case it will be returned straight away)
+ = (array) array of segments.
+ \*/
+ R.parsePathString = function (pathString) {
+ if (!pathString) {
+ return null;
+ }
+ var pth = paths(pathString);
+ if (pth.arr) {
+ return pathClone(pth.arr);
+ }
+
+ var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0},
+ data = [];
+ if (R.is(pathString, array) && R.is(pathString[0], array)) { // rough assumption
+ data = pathClone(pathString);
+ }
+ if (!data.length) {
+ Str(pathString).replace(pathCommand, function (a, b, c) {
+ var params = [],
+ name = b.toLowerCase();
+ c.replace(pathValues, function (a, b) {
+ b && params.push(+b);
+ });
+ if (name == "m" && params.length > 2) {
+ data.push([b][concat](params.splice(0, 2)));
+ name = "l";
+ b = b == "m" ? "l" : "L";
+ }
+ if (name == "r") {
+ data.push([b][concat](params));
+ } else while (params.length >= paramCounts[name]) {
+ data.push([b][concat](params.splice(0, paramCounts[name])));
+ if (!paramCounts[name]) {
+ break;
+ }
+ }
+ });
+ }
+ data.toString = R._path2string;
+ pth.arr = pathClone(data);
+ return data;
+ };
+ /*\
+ * Raphael.parseTransformString
+ [ method ]
+ **
+ * Utility method
+ **
+ * Parses given path string into an array of transformations.
+ > Parameters
+ - TString (string|array) transform string or array of transformations (in the last case it will be returned straight away)
+ = (array) array of transformations.
+ \*/
+ R.parseTransformString = cacher(function (TString) {
+ if (!TString) {
+ return null;
+ }
+ var paramCounts = {r: 3, s: 4, t: 2, m: 6},
+ data = [];
+ if (R.is(TString, array) && R.is(TString[0], array)) { // rough assumption
+ data = pathClone(TString);
+ }
+ if (!data.length) {
+ Str(TString).replace(tCommand, function (a, b, c) {
+ var params = [],
+ name = lowerCase.call(b);
+ c.replace(pathValues, function (a, b) {
+ b && params.push(+b);
+ });
+ data.push([b][concat](params));
+ });
+ }
+ data.toString = R._path2string;
+ return data;
+ });
+ // PATHS
+ var paths = function (ps) {
+ var p = paths.ps = paths.ps || {};
+ if (p[ps]) {
+ p[ps].sleep = 100;
+ } else {
+ p[ps] = {
+ sleep: 100
+ };
+ }
+ setTimeout(function () {
+ for (var key in p) if (p[has](key) && key != ps) {
+ p[key].sleep--;
+ !p[key].sleep && delete p[key];
+ }
+ });
+ return p[ps];
+ };
+ /*\
+ * Raphael.findDotsAtSegment
+ [ method ]
+ **
+ * Utility method
+ **
+ * Find dot coordinates on the given cubic bezier curve at the given t.
+ > Parameters
+ - p1x (number) x of the first point of the curve
+ - p1y (number) y of the first point of the curve
+ - c1x (number) x of the first anchor of the curve
+ - c1y (number) y of the first anchor of the curve
+ - c2x (number) x of the second anchor of the curve
+ - c2y (number) y of the second anchor of the curve
+ - p2x (number) x of the second point of the curve
+ - p2y (number) y of the second point of the curve
+ - t (number) position on the curve (0..1)
+ = (object) point information in format:
+ o {
+ o x: (number) x coordinate of the point
+ o y: (number) y coordinate of the point
+ o m: {
+ o x: (number) x coordinate of the left anchor
+ o y: (number) y coordinate of the left anchor
+ o }
+ o n: {
+ o x: (number) x coordinate of the right anchor
+ o y: (number) y coordinate of the right anchor
+ o }
+ o start: {
+ o x: (number) x coordinate of the start of the curve
+ o y: (number) y coordinate of the start of the curve
+ o }
+ o end: {
+ o x: (number) x coordinate of the end of the curve
+ o y: (number) y coordinate of the end of the curve
+ o }
+ o alpha: (number) angle of the curve derivative at the point
+ o }
+ \*/
+ R.findDotsAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
+ var t1 = 1 - t,
+ t13 = pow(t1, 3),
+ t12 = pow(t1, 2),
+ t2 = t * t,
+ t3 = t2 * t,
+ x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x,
+ y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y,
+ mx = p1x + 2 * t * (c1x - p1x) + t2 * (c2x - 2 * c1x + p1x),
+ my = p1y + 2 * t * (c1y - p1y) + t2 * (c2y - 2 * c1y + p1y),
+ nx = c1x + 2 * t * (c2x - c1x) + t2 * (p2x - 2 * c2x + c1x),
+ ny = c1y + 2 * t * (c2y - c1y) + t2 * (p2y - 2 * c2y + c1y),
+ ax = t1 * p1x + t * c1x,
+ ay = t1 * p1y + t * c1y,
+ cx = t1 * c2x + t * p2x,
+ cy = t1 * c2y + t * p2y,
+ alpha = (90 - math.atan2(mx - nx, my - ny) * 180 / PI);
+ (mx > nx || my < ny) && (alpha += 180);
+ return {
+ x: x,
+ y: y,
+ m: {x: mx, y: my},
+ n: {x: nx, y: ny},
+ start: {x: ax, y: ay},
+ end: {x: cx, y: cy},
+ alpha: alpha
+ };
+ };
+ /*\
+ * Raphael.bezierBBox
+ [ method ]
+ **
+ * Utility method
+ **
+ * Return bounding box of a given cubic bezier curve
+ > Parameters
+ - p1x (number) x of the first point of the curve
+ - p1y (number) y of the first point of the curve
+ - c1x (number) x of the first anchor of the curve
+ - c1y (number) y of the first anchor of the curve
+ - c2x (number) x of the second anchor of the curve
+ - c2y (number) y of the second anchor of the curve
+ - p2x (number) x of the second point of the curve
+ - p2y (number) y of the second point of the curve
+ * or
+ - bez (array) array of six points for bezier curve
+ = (object) point information in format:
+ o {
+ o min: {
+ o x: (number) x coordinate of the left point
+ o y: (number) y coordinate of the top point
+ o }
+ o max: {
+ o x: (number) x coordinate of the right point
+ o y: (number) y coordinate of the bottom point
+ o }
+ o }
+ \*/
+ R.bezierBBox = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
+ if (!R.is(p1x, "array")) {
+ p1x = [p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y];
+ }
+ var bbox = curveDim.apply(null, p1x);
+ return {
+ x: bbox.min.x,
+ y: bbox.min.y,
+ x2: bbox.max.x,
+ y2: bbox.max.y,
+ width: bbox.max.x - bbox.min.x,
+ height: bbox.max.y - bbox.min.y
+ };
+ };
+ /*\
+ * Raphael.isPointInsideBBox
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns `true` if given point is inside bounding boxes.
+ > Parameters
+ - bbox (string) bounding box
+ - x (string) x coordinate of the point
+ - y (string) y coordinate of the point
+ = (boolean) `true` if point inside
+ \*/
+ R.isPointInsideBBox = function (bbox, x, y) {
+ return x >= bbox.x && x <= bbox.x2 && y >= bbox.y && y <= bbox.y2;
+ };
+ /*\
+ * Raphael.isBBoxIntersect
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns `true` if two bounding boxes intersect
+ > Parameters
+ - bbox1 (string) first bounding box
+ - bbox2 (string) second bounding box
+ = (boolean) `true` if they intersect
+ \*/
+ R.isBBoxIntersect = function (bbox1, bbox2) {
+ var i = R.isPointInsideBBox;
+ return i(bbox2, bbox1.x, bbox1.y)
+ || i(bbox2, bbox1.x2, bbox1.y)
+ || i(bbox2, bbox1.x, bbox1.y2)
+ || i(bbox2, bbox1.x2, bbox1.y2)
+ || i(bbox1, bbox2.x, bbox2.y)
+ || i(bbox1, bbox2.x2, bbox2.y)
+ || i(bbox1, bbox2.x, bbox2.y2)
+ || i(bbox1, bbox2.x2, bbox2.y2)
+ || (bbox1.x < bbox2.x2 && bbox1.x > bbox2.x || bbox2.x < bbox1.x2 && bbox2.x > bbox1.x)
+ && (bbox1.y < bbox2.y2 && bbox1.y > bbox2.y || bbox2.y < bbox1.y2 && bbox2.y > bbox1.y);
+ };
+ function base3(t, p1, p2, p3, p4) {
+ var t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4,
+ t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3;
+ return t * t2 - 3 * p1 + 3 * p2;
+ }
+ function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) {
+ if (z == null) {
+ z = 1;
+ }
+ z = z > 1 ? 1 : z < 0 ? 0 : z;
+ var z2 = z / 2,
+ n = 12,
+ Tvalues = [-0.1252,0.1252,-0.3678,0.3678,-0.5873,0.5873,-0.7699,0.7699,-0.9041,0.9041,-0.9816,0.9816],
+ Cvalues = [0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472],
+ sum = 0;
+ for (var i = 0; i < n; i++) {
+ var ct = z2 * Tvalues[i] + z2,
+ xbase = base3(ct, x1, x2, x3, x4),
+ ybase = base3(ct, y1, y2, y3, y4),
+ comb = xbase * xbase + ybase * ybase;
+ sum += Cvalues[i] * math.sqrt(comb);
+ }
+ return z2 * sum;
+ }
+ function getTatLen(x1, y1, x2, y2, x3, y3, x4, y4, ll) {
+ if (ll < 0 || bezlen(x1, y1, x2, y2, x3, y3, x4, y4) < ll) {
+ return;
+ }
+ var t = 1,
+ step = t / 2,
+ t2 = t - step,
+ l,
+ e = .01;
+ l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
+ while (abs(l - ll) > e) {
+ step /= 2;
+ t2 += (l < ll ? 1 : -1) * step;
+ l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2);
+ }
+ return t2;
+ }
+ function intersect(x1, y1, x2, y2, x3, y3, x4, y4) {
+ if (
+ mmax(x1, x2) < mmin(x3, x4) ||
+ mmin(x1, x2) > mmax(x3, x4) ||
+ mmax(y1, y2) < mmin(y3, y4) ||
+ mmin(y1, y2) > mmax(y3, y4)
+ ) {
+ return;
+ }
+ var nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4),
+ ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4),
+ denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
+
+ if (!denominator) {
+ return;
+ }
+ var px = nx / denominator,
+ py = ny / denominator,
+ px2 = +px.toFixed(2),
+ py2 = +py.toFixed(2);
+ if (
+ px2 < +mmin(x1, x2).toFixed(2) ||
+ px2 > +mmax(x1, x2).toFixed(2) ||
+ px2 < +mmin(x3, x4).toFixed(2) ||
+ px2 > +mmax(x3, x4).toFixed(2) ||
+ py2 < +mmin(y1, y2).toFixed(2) ||
+ py2 > +mmax(y1, y2).toFixed(2) ||
+ py2 < +mmin(y3, y4).toFixed(2) ||
+ py2 > +mmax(y3, y4).toFixed(2)
+ ) {
+ return;
+ }
+ return {x: px, y: py};
+ }
+ function inter(bez1, bez2) {
+ return interHelper(bez1, bez2);
+ }
+ function interCount(bez1, bez2) {
+ return interHelper(bez1, bez2, 1);
+ }
+ function interHelper(bez1, bez2, justCount) {
+ var bbox1 = R.bezierBBox(bez1),
+ bbox2 = R.bezierBBox(bez2);
+ if (!R.isBBoxIntersect(bbox1, bbox2)) {
+ return justCount ? 0 : [];
+ }
+ var l1 = bezlen.apply(0, bez1),
+ l2 = bezlen.apply(0, bez2),
+ n1 = mmax(~~(l1 / 5), 1),
+ n2 = mmax(~~(l2 / 5), 1),
+ dots1 = [],
+ dots2 = [],
+ xy = {},
+ res = justCount ? 0 : [];
+ for (var i = 0; i < n1 + 1; i++) {
+ var p = R.findDotsAtSegment.apply(R, bez1.concat(i / n1));
+ dots1.push({x: p.x, y: p.y, t: i / n1});
+ }
+ for (i = 0; i < n2 + 1; i++) {
+ p = R.findDotsAtSegment.apply(R, bez2.concat(i / n2));
+ dots2.push({x: p.x, y: p.y, t: i / n2});
+ }
+ for (i = 0; i < n1; i++) {
+ for (var j = 0; j < n2; j++) {
+ var di = dots1[i],
+ di1 = dots1[i + 1],
+ dj = dots2[j],
+ dj1 = dots2[j + 1],
+ ci = abs(di1.x - di.x) < .001 ? "y" : "x",
+ cj = abs(dj1.x - dj.x) < .001 ? "y" : "x",
+ is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y);
+ if (is) {
+ if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) {
+ continue;
+ }
+ xy[is.x.toFixed(4)] = is.y.toFixed(4);
+ var t1 = di.t + abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t),
+ t2 = dj.t + abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t);
+ if (t1 >= 0 && t1 <= 1.001 && t2 >= 0 && t2 <= 1.001) {
+ if (justCount) {
+ res++;
+ } else {
+ res.push({
+ x: is.x,
+ y: is.y,
+ t1: mmin(t1, 1),
+ t2: mmin(t2, 1)
+ });
+ }
+ }
+ }
+ }
+ }
+ return res;
+ }
+ /*\
+ * Raphael.pathIntersection
+ [ method ]
+ **
+ * Utility method
+ **
+ * Finds intersections of two paths
+ > Parameters
+ - path1 (string) path string
+ - path2 (string) path string
+ = (array) dots of intersection
+ o [
+ o {
+ o x: (number) x coordinate of the point
+ o y: (number) y coordinate of the point
+ o t1: (number) t value for segment of path1
+ o t2: (number) t value for segment of path2
+ o segment1: (number) order number for segment of path1
+ o segment2: (number) order number for segment of path2
+ o bez1: (array) eight coordinates representing beziér curve for the segment of path1
+ o bez2: (array) eight coordinates representing beziér curve for the segment of path2
+ o }
+ o ]
+ \*/
+ R.pathIntersection = function (path1, path2) {
+ return interPathHelper(path1, path2);
+ };
+ R.pathIntersectionNumber = function (path1, path2) {
+ return interPathHelper(path1, path2, 1);
+ };
+ function interPathHelper(path1, path2, justCount) {
+ path1 = R._path2curve(path1);
+ path2 = R._path2curve(path2);
+ var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2,
+ res = justCount ? 0 : [];
+ for (var i = 0, ii = path1.length; i < ii; i++) {
+ var pi = path1[i];
+ if (pi[0] == "M") {
+ x1 = x1m = pi[1];
+ y1 = y1m = pi[2];
+ } else {
+ if (pi[0] == "C") {
+ bez1 = [x1, y1].concat(pi.slice(1));
+ x1 = bez1[6];
+ y1 = bez1[7];
+ } else {
+ bez1 = [x1, y1, x1, y1, x1m, y1m, x1m, y1m];
+ x1 = x1m;
+ y1 = y1m;
+ }
+ for (var j = 0, jj = path2.length; j < jj; j++) {
+ var pj = path2[j];
+ if (pj[0] == "M") {
+ x2 = x2m = pj[1];
+ y2 = y2m = pj[2];
+ } else {
+ if (pj[0] == "C") {
+ bez2 = [x2, y2].concat(pj.slice(1));
+ x2 = bez2[6];
+ y2 = bez2[7];
+ } else {
+ bez2 = [x2, y2, x2, y2, x2m, y2m, x2m, y2m];
+ x2 = x2m;
+ y2 = y2m;
+ }
+ var intr = interHelper(bez1, bez2, justCount);
+ if (justCount) {
+ res += intr;
+ } else {
+ for (var k = 0, kk = intr.length; k < kk; k++) {
+ intr[k].segment1 = i;
+ intr[k].segment2 = j;
+ intr[k].bez1 = bez1;
+ intr[k].bez2 = bez2;
+ }
+ res = res.concat(intr);
+ }
+ }
+ }
+ }
+ }
+ return res;
+ }
+ /*\
+ * Raphael.isPointInsidePath
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns `true` if given point is inside a given closed path.
+ > Parameters
+ - path (string) path string
+ - x (number) x of the point
+ - y (number) y of the point
+ = (boolean) true, if point is inside the path
+ \*/
+ R.isPointInsidePath = function (path, x, y) {
+ var bbox = R.pathBBox(path);
+ return R.isPointInsideBBox(bbox, x, y) &&
+ interPathHelper(path, [["M", x, y], ["H", bbox.x2 + 10]], 1) % 2 == 1;
+ };
+ R._removedFactory = function (methodname) {
+ return function () {
+ eve("raphael.log", null, "Rapha\xebl: you are calling to method \u201c" + methodname + "\u201d of removed object", methodname);
+ };
+ };
+ /*\
+ * Raphael.pathBBox
+ [ method ]
+ **
+ * Utility method
+ **
+ * Return bounding box of a given path
+ > Parameters
+ - path (string) path string
+ = (object) bounding box
+ o {
+ o x: (number) x coordinate of the left top point of the box
+ o y: (number) y coordinate of the left top point of the box
+ o x2: (number) x coordinate of the right bottom point of the box
+ o y2: (number) y coordinate of the right bottom point of the box
+ o width: (number) width of the box
+ o height: (number) height of the box
+ o cx: (number) x coordinate of the center of the box
+ o cy: (number) y coordinate of the center of the box
+ o }
+ \*/
+ var pathDimensions = R.pathBBox = function (path) {
+ var pth = paths(path);
+ if (pth.bbox) {
+ return clone(pth.bbox);
+ }
+ if (!path) {
+ return {x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0};
+ }
+ path = path2curve(path);
+ var x = 0,
+ y = 0,
+ X = [],
+ Y = [],
+ p;
+ for (var i = 0, ii = path.length; i < ii; i++) {
+ p = path[i];
+ if (p[0] == "M") {
+ x = p[1];
+ y = p[2];
+ X.push(x);
+ Y.push(y);
+ } else {
+ var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+ X = X[concat](dim.min.x, dim.max.x);
+ Y = Y[concat](dim.min.y, dim.max.y);
+ x = p[5];
+ y = p[6];
+ }
+ }
+ var xmin = mmin[apply](0, X),
+ ymin = mmin[apply](0, Y),
+ xmax = mmax[apply](0, X),
+ ymax = mmax[apply](0, Y),
+ width = xmax - xmin,
+ height = ymax - ymin,
+ bb = {
+ x: xmin,
+ y: ymin,
+ x2: xmax,
+ y2: ymax,
+ width: width,
+ height: height,
+ cx: xmin + width / 2,
+ cy: ymin + height / 2
+ };
+ pth.bbox = clone(bb);
+ return bb;
+ },
+ pathClone = function (pathArray) {
+ var res = clone(pathArray);
+ res.toString = R._path2string;
+ return res;
+ },
+ pathToRelative = R._pathToRelative = function (pathArray) {
+ var pth = paths(pathArray);
+ if (pth.rel) {
+ return pathClone(pth.rel);
+ }
+ if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
+ pathArray = R.parsePathString(pathArray);
+ }
+ var res = [],
+ x = 0,
+ y = 0,
+ mx = 0,
+ my = 0,
+ start = 0;
+ if (pathArray[0][0] == "M") {
+ x = pathArray[0][1];
+ y = pathArray[0][2];
+ mx = x;
+ my = y;
+ start++;
+ res.push(["M", x, y]);
+ }
+ for (var i = start, ii = pathArray.length; i < ii; i++) {
+ var r = res[i] = [],
+ pa = pathArray[i];
+ if (pa[0] != lowerCase.call(pa[0])) {
+ r[0] = lowerCase.call(pa[0]);
+ switch (r[0]) {
+ case "a":
+ r[1] = pa[1];
+ r[2] = pa[2];
+ r[3] = pa[3];
+ r[4] = pa[4];
+ r[5] = pa[5];
+ r[6] = +(pa[6] - x).toFixed(3);
+ r[7] = +(pa[7] - y).toFixed(3);
+ break;
+ case "v":
+ r[1] = +(pa[1] - y).toFixed(3);
+ break;
+ case "m":
+ mx = pa[1];
+ my = pa[2];
+ default:
+ for (var j = 1, jj = pa.length; j < jj; j++) {
+ r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3);
+ }
+ }
+ } else {
+ r = res[i] = [];
+ if (pa[0] == "m") {
+ mx = pa[1] + x;
+ my = pa[2] + y;
+ }
+ for (var k = 0, kk = pa.length; k < kk; k++) {
+ res[i][k] = pa[k];
+ }
+ }
+ var len = res[i].length;
+ switch (res[i][0]) {
+ case "z":
+ x = mx;
+ y = my;
+ break;
+ case "h":
+ x += +res[i][len - 1];
+ break;
+ case "v":
+ y += +res[i][len - 1];
+ break;
+ default:
+ x += +res[i][len - 2];
+ y += +res[i][len - 1];
+ }
+ }
+ res.toString = R._path2string;
+ pth.rel = pathClone(res);
+ return res;
+ },
+ pathToAbsolute = R._pathToAbsolute = function (pathArray) {
+ var pth = paths(pathArray);
+ if (pth.abs) {
+ return pathClone(pth.abs);
+ }
+ if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption
+ pathArray = R.parsePathString(pathArray);
+ }
+ if (!pathArray || !pathArray.length) {
+ return [["M", 0, 0]];
+ }
+ var res = [],
+ x = 0,
+ y = 0,
+ mx = 0,
+ my = 0,
+ start = 0;
+ if (pathArray[0][0] == "M") {
+ x = +pathArray[0][1];
+ y = +pathArray[0][2];
+ mx = x;
+ my = y;
+ start++;
+ res[0] = ["M", x, y];
+ }
+ var crz = pathArray.length == 3 && pathArray[0][0] == "M" && pathArray[1][0].toUpperCase() == "R" && pathArray[2][0].toUpperCase() == "Z";
+ for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) {
+ res.push(r = []);
+ pa = pathArray[i];
+ if (pa[0] != upperCase.call(pa[0])) {
+ r[0] = upperCase.call(pa[0]);
+ switch (r[0]) {
+ case "A":
+ r[1] = pa[1];
+ r[2] = pa[2];
+ r[3] = pa[3];
+ r[4] = pa[4];
+ r[5] = pa[5];
+ r[6] = +(pa[6] + x);
+ r[7] = +(pa[7] + y);
+ break;
+ case "V":
+ r[1] = +pa[1] + y;
+ break;
+ case "H":
+ r[1] = +pa[1] + x;
+ break;
+ case "R":
+ var dots = [x, y][concat](pa.slice(1));
+ for (var j = 2, jj = dots.length; j < jj; j++) {
+ dots[j] = +dots[j] + x;
+ dots[++j] = +dots[j] + y;
+ }
+ res.pop();
+ res = res[concat](catmullRom2bezier(dots, crz));
+ break;
+ case "M":
+ mx = +pa[1] + x;
+ my = +pa[2] + y;
+ default:
+ for (j = 1, jj = pa.length; j < jj; j++) {
+ r[j] = +pa[j] + ((j % 2) ? x : y);
+ }
+ }
+ } else if (pa[0] == "R") {
+ dots = [x, y][concat](pa.slice(1));
+ res.pop();
+ res = res[concat](catmullRom2bezier(dots, crz));
+ r = ["R"][concat](pa.slice(-2));
+ } else {
+ for (var k = 0, kk = pa.length; k < kk; k++) {
+ r[k] = pa[k];
+ }
+ }
+ switch (r[0]) {
+ case "Z":
+ x = mx;
+ y = my;
+ break;
+ case "H":
+ x = r[1];
+ break;
+ case "V":
+ y = r[1];
+ break;
+ case "M":
+ mx = r[r.length - 2];
+ my = r[r.length - 1];
+ default:
+ x = r[r.length - 2];
+ y = r[r.length - 1];
+ }
+ }
+ res.toString = R._path2string;
+ pth.abs = pathClone(res);
+ return res;
+ },
+ l2c = function (x1, y1, x2, y2) {
+ return [x1, y1, x2, y2, x2, y2];
+ },
+ q2c = function (x1, y1, ax, ay, x2, y2) {
+ var _13 = 1 / 3,
+ _23 = 2 / 3;
+ return [
+ _13 * x1 + _23 * ax,
+ _13 * y1 + _23 * ay,
+ _13 * x2 + _23 * ax,
+ _13 * y2 + _23 * ay,
+ x2,
+ y2
+ ];
+ },
+ a2c = function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
+ // for more information of where this math came from visit:
+ // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
+ var _120 = PI * 120 / 180,
+ rad = PI / 180 * (+angle || 0),
+ res = [],
+ xy,
+ rotate = cacher(function (x, y, rad) {
+ var X = x * math.cos(rad) - y * math.sin(rad),
+ Y = x * math.sin(rad) + y * math.cos(rad);
+ return {x: X, y: Y};
+ });
+ if (!recursive) {
+ xy = rotate(x1, y1, -rad);
+ x1 = xy.x;
+ y1 = xy.y;
+ xy = rotate(x2, y2, -rad);
+ x2 = xy.x;
+ y2 = xy.y;
+ var cos = math.cos(PI / 180 * angle),
+ sin = math.sin(PI / 180 * angle),
+ x = (x1 - x2) / 2,
+ y = (y1 - y2) / 2;
+ var h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
+ if (h > 1) {
+ h = math.sqrt(h);
+ rx = h * rx;
+ ry = h * ry;
+ }
+ var rx2 = rx * rx,
+ ry2 = ry * ry,
+ k = (large_arc_flag == sweep_flag ? -1 : 1) *
+ math.sqrt(abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))),
+ cx = k * rx * y / ry + (x1 + x2) / 2,
+ cy = k * -ry * x / rx + (y1 + y2) / 2,
+ f1 = math.asin(((y1 - cy) / ry).toFixed(9)),
+ f2 = math.asin(((y2 - cy) / ry).toFixed(9));
+
+ f1 = x1 < cx ? PI - f1 : f1;
+ f2 = x2 < cx ? PI - f2 : f2;
+ f1 < 0 && (f1 = PI * 2 + f1);
+ f2 < 0 && (f2 = PI * 2 + f2);
+ if (sweep_flag && f1 > f2) {
+ f1 = f1 - PI * 2;
+ }
+ if (!sweep_flag && f2 > f1) {
+ f2 = f2 - PI * 2;
+ }
+ } else {
+ f1 = recursive[0];
+ f2 = recursive[1];
+ cx = recursive[2];
+ cy = recursive[3];
+ }
+ var df = f2 - f1;
+ if (abs(df) > _120) {
+ var f2old = f2,
+ x2old = x2,
+ y2old = y2;
+ f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
+ x2 = cx + rx * math.cos(f2);
+ y2 = cy + ry * math.sin(f2);
+ res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
+ }
+ df = f2 - f1;
+ var c1 = math.cos(f1),
+ s1 = math.sin(f1),
+ c2 = math.cos(f2),
+ s2 = math.sin(f2),
+ t = math.tan(df / 4),
+ hx = 4 / 3 * rx * t,
+ hy = 4 / 3 * ry * t,
+ m1 = [x1, y1],
+ m2 = [x1 + hx * s1, y1 - hy * c1],
+ m3 = [x2 + hx * s2, y2 - hy * c2],
+ m4 = [x2, y2];
+ m2[0] = 2 * m1[0] - m2[0];
+ m2[1] = 2 * m1[1] - m2[1];
+ if (recursive) {
+ return [m2, m3, m4][concat](res);
+ } else {
+ res = [m2, m3, m4][concat](res).join()[split](",");
+ var newres = [];
+ for (var i = 0, ii = res.length; i < ii; i++) {
+ newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x;
+ }
+ return newres;
+ }
+ },
+ findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
+ var t1 = 1 - t;
+ return {
+ x: pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x,
+ y: pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y
+ };
+ },
+ curveDim = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
+ var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
+ b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
+ c = p1x - c1x,
+ t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a,
+ t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a,
+ y = [p1y, p2y],
+ x = [p1x, p2x],
+ dot;
+ abs(t1) > "1e12" && (t1 = .5);
+ abs(t2) > "1e12" && (t2 = .5);
+ if (t1 > 0 && t1 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ if (t2 > 0 && t2 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
+ b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
+ c = p1y - c1y;
+ t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a;
+ t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a;
+ abs(t1) > "1e12" && (t1 = .5);
+ abs(t2) > "1e12" && (t2 = .5);
+ if (t1 > 0 && t1 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ if (t2 > 0 && t2 < 1) {
+ dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ return {
+ min: {x: mmin[apply](0, x), y: mmin[apply](0, y)},
+ max: {x: mmax[apply](0, x), y: mmax[apply](0, y)}
+ };
+ }),
+ path2curve = R._path2curve = cacher(function (path, path2) {
+ var pth = !path2 && paths(path);
+ if (!path2 && pth.curve) {
+ return pathClone(pth.curve);
+ }
+ var p = pathToAbsolute(path),
+ p2 = path2 && pathToAbsolute(path2),
+ attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+ attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+ processPath = function (path, d, pcom) {
+ var nx, ny, tq = {T:1, Q:1};
+ if (!path) {
+ return ["C", d.x, d.y, d.x, d.y, d.x, d.y];
+ }
+ !(path[0] in tq) && (d.qx = d.qy = null);
+ switch (path[0]) {
+ case "M":
+ d.X = path[1];
+ d.Y = path[2];
+ break;
+ case "A":
+ path = ["C"][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1))));
+ break;
+ case "S":
+ if (pcom == "C" || pcom == "S") { // In "S" case we have to take into account, if the previous command is C/S.
+ nx = d.x * 2 - d.bx; // And reflect the previous
+ ny = d.y * 2 - d.by; // command's control point relative to the current point.
+ }
+ else { // or some else or nothing
+ nx = d.x;
+ ny = d.y;
+ }
+ path = ["C", nx, ny][concat](path.slice(1));
+ break;
+ case "T":
+ if (pcom == "Q" || pcom == "T") { // In "T" case we have to take into account, if the previous command is Q/T.
+ d.qx = d.x * 2 - d.qx; // And make a reflection similar
+ d.qy = d.y * 2 - d.qy; // to case "S".
+ }
+ else { // or something else or nothing
+ d.qx = d.x;
+ d.qy = d.y;
+ }
+ path = ["C"][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2]));
+ break;
+ case "Q":
+ d.qx = path[1];
+ d.qy = path[2];
+ path = ["C"][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4]));
+ break;
+ case "L":
+ path = ["C"][concat](l2c(d.x, d.y, path[1], path[2]));
+ break;
+ case "H":
+ path = ["C"][concat](l2c(d.x, d.y, path[1], d.y));
+ break;
+ case "V":
+ path = ["C"][concat](l2c(d.x, d.y, d.x, path[1]));
+ break;
+ case "Z":
+ path = ["C"][concat](l2c(d.x, d.y, d.X, d.Y));
+ break;
+ }
+ return path;
+ },
+ fixArc = function (pp, i) {
+ if (pp[i].length > 7) {
+ pp[i].shift();
+ var pi = pp[i];
+ while (pi.length) {
+ pcoms1[i]="A"; // if created multiple C:s, their original seg is saved
+ p2 && (pcoms2[i]="A"); // the same as above
+ pp.splice(i++, 0, ["C"][concat](pi.splice(0, 6)));
+ }
+ pp.splice(i, 1);
+ ii = mmax(p.length, p2 && p2.length || 0);
+ }
+ },
+ fixM = function (path1, path2, a1, a2, i) {
+ if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
+ path2.splice(i, 0, ["M", a2.x, a2.y]);
+ a1.bx = 0;
+ a1.by = 0;
+ a1.x = path1[i][1];
+ a1.y = path1[i][2];
+ ii = mmax(p.length, p2 && p2.length || 0);
+ }
+ },
+ pcoms1 = [], // path commands of original path p
+ pcoms2 = [], // path commands of original path p2
+ pfirst = "", // temporary holder for original path command
+ pcom = ""; // holder for previous path command of original path
+ for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) {
+ p[i] && (pfirst = p[i][0]); // save current path command
+
+ if (pfirst != "C") // C is not saved yet, because it may be result of conversion
+ {
+ pcoms1[i] = pfirst; // Save current path command
+ i && ( pcom = pcoms1[i-1]); // Get previous path command pcom
+ }
+ p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath
+
+ if (pcoms1[i] != "A" && pfirst == "C") pcoms1[i] = "C"; // A is the only command
+ // which may produce multiple C:s
+ // so we have to make sure that C is also C in original path
+
+ fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1
+
+ if (p2) { // the same procedures is done to p2
+ p2[i] && (pfirst = p2[i][0]);
+ if (pfirst != "C")
+ {
+ pcoms2[i] = pfirst;
+ i && (pcom = pcoms2[i-1]);
+ }
+ p2[i] = processPath(p2[i], attrs2, pcom);
+
+ if (pcoms2[i]!="A" && pfirst=="C") pcoms2[i]="C";
+
+ fixArc(p2, i);
+ }
+ fixM(p, p2, attrs, attrs2, i);
+ fixM(p2, p, attrs2, attrs, i);
+ var seg = p[i],
+ seg2 = p2 && p2[i],
+ seglen = seg.length,
+ seg2len = p2 && seg2.length;
+ attrs.x = seg[seglen - 2];
+ attrs.y = seg[seglen - 1];
+ attrs.bx = toFloat(seg[seglen - 4]) || attrs.x;
+ attrs.by = toFloat(seg[seglen - 3]) || attrs.y;
+ attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x);
+ attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y);
+ attrs2.x = p2 && seg2[seg2len - 2];
+ attrs2.y = p2 && seg2[seg2len - 1];
+ }
+ if (!p2) {
+ pth.curve = pathClone(p);
+ }
+ return p2 ? [p, p2] : p;
+ }, null, pathClone),
+ parseDots = R._parseDots = cacher(function (gradient) {
+ var dots = [];
+ for (var i = 0, ii = gradient.length; i < ii; i++) {
+ var dot = {},
+ par = gradient[i].match(/^([^:]*):?([\d\.]*)/);
+ dot.color = R.getRGB(par[1]);
+ if (dot.color.error) {
+ return null;
+ }
+ dot.color = dot.color.hex;
+ par[2] && (dot.offset = par[2] + "%");
+ dots.push(dot);
+ }
+ for (i = 1, ii = dots.length - 1; i < ii; i++) {
+ if (!dots[i].offset) {
+ var start = toFloat(dots[i - 1].offset || 0),
+ end = 0;
+ for (var j = i + 1; j < ii; j++) {
+ if (dots[j].offset) {
+ end = dots[j].offset;
+ break;
+ }
+ }
+ if (!end) {
+ end = 100;
+ j = ii;
+ }
+ end = toFloat(end);
+ var d = (end - start) / (j - i + 1);
+ for (; i < j; i++) {
+ start += d;
+ dots[i].offset = start + "%";
+ }
+ }
+ }
+ return dots;
+ }),
+ tear = R._tear = function (el, paper) {
+ el == paper.top && (paper.top = el.prev);
+ el == paper.bottom && (paper.bottom = el.next);
+ el.next && (el.next.prev = el.prev);
+ el.prev && (el.prev.next = el.next);
+ },
+ tofront = R._tofront = function (el, paper) {
+ if (paper.top === el) {
+ return;
+ }
+ tear(el, paper);
+ el.next = null;
+ el.prev = paper.top;
+ paper.top.next = el;
+ paper.top = el;
+ },
+ toback = R._toback = function (el, paper) {
+ if (paper.bottom === el) {
+ return;
+ }
+ tear(el, paper);
+ el.next = paper.bottom;
+ el.prev = null;
+ paper.bottom.prev = el;
+ paper.bottom = el;
+ },
+ insertafter = R._insertafter = function (el, el2, paper) {
+ tear(el, paper);
+ el2 == paper.top && (paper.top = el);
+ el2.next && (el2.next.prev = el);
+ el.next = el2.next;
+ el.prev = el2;
+ el2.next = el;
+ },
+ insertbefore = R._insertbefore = function (el, el2, paper) {
+ tear(el, paper);
+ el2 == paper.bottom && (paper.bottom = el);
+ el2.prev && (el2.prev.next = el);
+ el.prev = el2.prev;
+ el2.prev = el;
+ el.next = el2;
+ },
+ /*\
+ * Raphael.toMatrix
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns matrix of transformations applied to a given path
+ > Parameters
+ - path (string) path string
+ - transform (string|array) transformation string
+ = (object) @Matrix
+ \*/
+ toMatrix = R.toMatrix = function (path, transform) {
+ var bb = pathDimensions(path),
+ el = {
+ _: {
+ transform: E
+ },
+ getBBox: function () {
+ return bb;
+ }
+ };
+ extractTransform(el, transform);
+ return el.matrix;
+ },
+ /*\
+ * Raphael.transformPath
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns path transformed by a given transformation
+ > Parameters
+ - path (string) path string
+ - transform (string|array) transformation string
+ = (string) path
+ \*/
+ transformPath = R.transformPath = function (path, transform) {
+ return mapPath(path, toMatrix(path, transform));
+ },
+ extractTransform = R._extractTransform = function (el, tstr) {
+ if (tstr == null) {
+ return el._.transform;
+ }
+ tstr = Str(tstr).replace(/\.{3}|\u2026/g, el._.transform || E);
+ var tdata = R.parseTransformString(tstr),
+ deg = 0,
+ dx = 0,
+ dy = 0,
+ sx = 1,
+ sy = 1,
+ _ = el._,
+ m = new Matrix;
+ _.transform = tdata || [];
+ if (tdata) {
+ for (var i = 0, ii = tdata.length; i < ii; i++) {
+ var t = tdata[i],
+ tlen = t.length,
+ command = Str(t[0]).toLowerCase(),
+ absolute = t[0] != command,
+ inver = absolute ? m.invert() : 0,
+ x1,
+ y1,
+ x2,
+ y2,
+ bb;
+ if (command == "t" && tlen == 3) {
+ if (absolute) {
+ x1 = inver.x(0, 0);
+ y1 = inver.y(0, 0);
+ x2 = inver.x(t[1], t[2]);
+ y2 = inver.y(t[1], t[2]);
+ m.translate(x2 - x1, y2 - y1);
+ } else {
+ m.translate(t[1], t[2]);
+ }
+ } else if (command == "r") {
+ if (tlen == 2) {
+ bb = bb || el.getBBox(1);
+ m.rotate(t[1], bb.x + bb.width / 2, bb.y + bb.height / 2);
+ deg += t[1];
+ } else if (tlen == 4) {
+ if (absolute) {
+ x2 = inver.x(t[2], t[3]);
+ y2 = inver.y(t[2], t[3]);
+ m.rotate(t[1], x2, y2);
+ } else {
+ m.rotate(t[1], t[2], t[3]);
+ }
+ deg += t[1];
+ }
+ } else if (command == "s") {
+ if (tlen == 2 || tlen == 3) {
+ bb = bb || el.getBBox(1);
+ m.scale(t[1], t[tlen - 1], bb.x + bb.width / 2, bb.y + bb.height / 2);
+ sx *= t[1];
+ sy *= t[tlen - 1];
+ } else if (tlen == 5) {
+ if (absolute) {
+ x2 = inver.x(t[3], t[4]);
+ y2 = inver.y(t[3], t[4]);
+ m.scale(t[1], t[2], x2, y2);
+ } else {
+ m.scale(t[1], t[2], t[3], t[4]);
+ }
+ sx *= t[1];
+ sy *= t[2];
+ }
+ } else if (command == "m" && tlen == 7) {
+ m.add(t[1], t[2], t[3], t[4], t[5], t[6]);
+ }
+ _.dirtyT = 1;
+ el.matrix = m;
+ }
+ }
+
+ /*\
+ * Element.matrix
+ [ property (object) ]
+ **
+ * Keeps @Matrix object, which represents element transformation
+ \*/
+ el.matrix = m;
+
+ _.sx = sx;
+ _.sy = sy;
+ _.deg = deg;
+ _.dx = dx = m.e;
+ _.dy = dy = m.f;
+
+ if (sx == 1 && sy == 1 && !deg && _.bbox) {
+ _.bbox.x += +dx;
+ _.bbox.y += +dy;
+ } else {
+ _.dirtyT = 1;
+ }
+ },
+ getEmpty = function (item) {
+ var l = item[0];
+ switch (l.toLowerCase()) {
+ case "t": return [l, 0, 0];
+ case "m": return [l, 1, 0, 0, 1, 0, 0];
+ case "r": if (item.length == 4) {
+ return [l, 0, item[2], item[3]];
+ } else {
+ return [l, 0];
+ }
+ case "s": if (item.length == 5) {
+ return [l, 1, 1, item[3], item[4]];
+ } else if (item.length == 3) {
+ return [l, 1, 1];
+ } else {
+ return [l, 1];
+ }
+ }
+ },
+ equaliseTransform = R._equaliseTransform = function (t1, t2) {
+ t2 = Str(t2).replace(/\.{3}|\u2026/g, t1);
+ t1 = R.parseTransformString(t1) || [];
+ t2 = R.parseTransformString(t2) || [];
+ var maxlength = mmax(t1.length, t2.length),
+ from = [],
+ to = [],
+ i = 0, j, jj,
+ tt1, tt2;
+ for (; i < maxlength; i++) {
+ tt1 = t1[i] || getEmpty(t2[i]);
+ tt2 = t2[i] || getEmpty(tt1);
+ if ((tt1[0] != tt2[0]) ||
+ (tt1[0].toLowerCase() == "r" && (tt1[2] != tt2[2] || tt1[3] != tt2[3])) ||
+ (tt1[0].toLowerCase() == "s" && (tt1[3] != tt2[3] || tt1[4] != tt2[4]))
+ ) {
+ return;
+ }
+ from[i] = [];
+ to[i] = [];
+ for (j = 0, jj = mmax(tt1.length, tt2.length); j < jj; j++) {
+ j in tt1 && (from[i][j] = tt1[j]);
+ j in tt2 && (to[i][j] = tt2[j]);
+ }
+ }
+ return {
+ from: from,
+ to: to
+ };
+ };
+ R._getContainer = function (x, y, w, h) {
+ var container;
+ container = h == null && !R.is(x, "object") ? g.doc.getElementById(x) : x;
+ if (container == null) {
+ return;
+ }
+ if (container.tagName) {
+ if (y == null) {
+ return {
+ container: container,
+ width: container.style.pixelWidth || container.offsetWidth,
+ height: container.style.pixelHeight || container.offsetHeight
+ };
+ } else {
+ return {
+ container: container,
+ width: y,
+ height: w
+ };
+ }
+ }
+ return {
+ container: 1,
+ x: x,
+ y: y,
+ width: w,
+ height: h
+ };
+ };
+ /*\
+ * Raphael.pathToRelative
+ [ method ]
+ **
+ * Utility method
+ **
+ * Converts path to relative form
+ > Parameters
+ - pathString (string|array) path string or array of segments
+ = (array) array of segments.
+ \*/
+ R.pathToRelative = pathToRelative;
+ R._engine = {};
+ /*\
+ * Raphael.path2curve
+ [ method ]
+ **
+ * Utility method
+ **
+ * Converts path to a new path where all segments are cubic bezier curves.
+ > Parameters
+ - pathString (string|array) path string or array of segments
+ = (array) array of segments.
+ \*/
+ R.path2curve = path2curve;
+ /*\
+ * Raphael.matrix
+ [ method ]
+ **
+ * Utility method
+ **
+ * Returns matrix based on given parameters.
+ > Parameters
+ - a (number)
+ - b (number)
+ - c (number)
+ - d (number)
+ - e (number)
+ - f (number)
+ = (object) @Matrix
+ \*/
+ R.matrix = function (a, b, c, d, e, f) {
+ return new Matrix(a, b, c, d, e, f);
+ };
+ function Matrix(a, b, c, d, e, f) {
+ if (a != null) {
+ this.a = +a;
+ this.b = +b;
+ this.c = +c;
+ this.d = +d;
+ this.e = +e;
+ this.f = +f;
+ } else {
+ this.a = 1;
+ this.b = 0;
+ this.c = 0;
+ this.d = 1;
+ this.e = 0;
+ this.f = 0;
+ }
+ }
+ (function (matrixproto) {
+ /*\
+ * Matrix.add
+ [ method ]
+ **
+ * Adds given matrix to existing one.
+ > Parameters
+ - a (number)
+ - b (number)
+ - c (number)
+ - d (number)
+ - e (number)
+ - f (number)
+ or
+ - matrix (object) @Matrix
+ \*/
+ matrixproto.add = function (a, b, c, d, e, f) {
+ var out = [[], [], []],
+ m = [[this.a, this.c, this.e], [this.b, this.d, this.f], [0, 0, 1]],
+ matrix = [[a, c, e], [b, d, f], [0, 0, 1]],
+ x, y, z, res;
+
+ if (a && a instanceof Matrix) {
+ matrix = [[a.a, a.c, a.e], [a.b, a.d, a.f], [0, 0, 1]];
+ }
+
+ for (x = 0; x < 3; x++) {
+ for (y = 0; y < 3; y++) {
+ res = 0;
+ for (z = 0; z < 3; z++) {
+ res += m[x][z] * matrix[z][y];
+ }
+ out[x][y] = res;
+ }
+ }
+ this.a = out[0][0];
+ this.b = out[1][0];
+ this.c = out[0][1];
+ this.d = out[1][1];
+ this.e = out[0][2];
+ this.f = out[1][2];
+ };
+ /*\
+ * Matrix.invert
+ [ method ]
+ **
+ * Returns inverted version of the matrix
+ = (object) @Matrix
+ \*/
+ matrixproto.invert = function () {
+ var me = this,
+ x = me.a * me.d - me.b * me.c;
+ return new Matrix(me.d / x, -me.b / x, -me.c / x, me.a / x, (me.c * me.f - me.d * me.e) / x, (me.b * me.e - me.a * me.f) / x);
+ };
+ /*\
+ * Matrix.clone
+ [ method ]
+ **
+ * Returns copy of the matrix
+ = (object) @Matrix
+ \*/
+ matrixproto.clone = function () {
+ return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f);
+ };
+ /*\
+ * Matrix.translate
+ [ method ]
+ **
+ * Translate the matrix
+ > Parameters
+ - x (number)
+ - y (number)
+ \*/
+ matrixproto.translate = function (x, y) {
+ this.add(1, 0, 0, 1, x, y);
+ };
+ /*\
+ * Matrix.scale
+ [ method ]
+ **
+ * Scales the matrix
+ > Parameters
+ - x (number)
+ - y (number) #optional
+ - cx (number) #optional
+ - cy (number) #optional
+ \*/
+ matrixproto.scale = function (x, y, cx, cy) {
+ y == null && (y = x);
+ (cx || cy) && this.add(1, 0, 0, 1, cx, cy);
+ this.add(x, 0, 0, y, 0, 0);
+ (cx || cy) && this.add(1, 0, 0, 1, -cx, -cy);
+ };
+ /*\
+ * Matrix.rotate
+ [ method ]
+ **
+ * Rotates the matrix
+ > Parameters
+ - a (number)
+ - x (number)
+ - y (number)
+ \*/
+ matrixproto.rotate = function (a, x, y) {
+ a = R.rad(a);
+ x = x || 0;
+ y = y || 0;
+ var cos = +math.cos(a).toFixed(9),
+ sin = +math.sin(a).toFixed(9);
+ this.add(cos, sin, -sin, cos, x, y);
+ this.add(1, 0, 0, 1, -x, -y);
+ };
+ /*\
+ * Matrix.x
+ [ method ]
+ **
+ * Return x coordinate for given point after transformation described by the matrix. See also @Matrix.y
+ > Parameters
+ - x (number)
+ - y (number)
+ = (number) x
+ \*/
+ matrixproto.x = function (x, y) {
+ return x * this.a + y * this.c + this.e;
+ };
+ /*\
+ * Matrix.y
+ [ method ]
+ **
+ * Return y coordinate for given point after transformation described by the matrix. See also @Matrix.x
+ > Parameters
+ - x (number)
+ - y (number)
+ = (number) y
+ \*/
+ matrixproto.y = function (x, y) {
+ return x * this.b + y * this.d + this.f;
+ };
+ matrixproto.get = function (i) {
+ return +this[Str.fromCharCode(97 + i)].toFixed(4);
+ };
+ matrixproto.toString = function () {
+ return R.svg ?
+ "matrix(" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)].join() + ")" :
+ [this.get(0), this.get(2), this.get(1), this.get(3), 0, 0].join();
+ };
+ matrixproto.toFilter = function () {
+ return "progid:DXImageTransform.Microsoft.Matrix(M11=" + this.get(0) +
+ ", M12=" + this.get(2) + ", M21=" + this.get(1) + ", M22=" + this.get(3) +
+ ", Dx=" + this.get(4) + ", Dy=" + this.get(5) + ", sizingmethod='auto expand')";
+ };
+ matrixproto.offset = function () {
+ return [this.e.toFixed(4), this.f.toFixed(4)];
+ };
+ function norm(a) {
+ return a[0] * a[0] + a[1] * a[1];
+ }
+ function normalize(a) {
+ var mag = math.sqrt(norm(a));
+ a[0] && (a[0] /= mag);
+ a[1] && (a[1] /= mag);
+ }
+ /*\
+ * Matrix.split
+ [ method ]
+ **
+ * Splits matrix into primitive transformations
+ = (object) in format:
+ o dx (number) translation by x
+ o dy (number) translation by y
+ o scalex (number) scale by x
+ o scaley (number) scale by y
+ o shear (number) shear
+ o rotate (number) rotation in deg
+ o isSimple (boolean) could it be represented via simple transformations
+ \*/
+ matrixproto.split = function () {
+ var out = {};
+ // translation
+ out.dx = this.e;
+ out.dy = this.f;
+
+ // scale and shear
+ var row = [[this.a, this.c], [this.b, this.d]];
+ out.scalex = math.sqrt(norm(row[0]));
+ normalize(row[0]);
+
+ out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1];
+ row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear];
+
+ out.scaley = math.sqrt(norm(row[1]));
+ normalize(row[1]);
+ out.shear /= out.scaley;
+
+ // rotation
+ var sin = -row[0][1],
+ cos = row[1][1];
+ if (cos < 0) {
+ out.rotate = R.deg(math.acos(cos));
+ if (sin < 0) {
+ out.rotate = 360 - out.rotate;
+ }
+ } else {
+ out.rotate = R.deg(math.asin(sin));
+ }
+
+ out.isSimple = !+out.shear.toFixed(9) && (out.scalex.toFixed(9) == out.scaley.toFixed(9) || !out.rotate);
+ out.isSuperSimple = !+out.shear.toFixed(9) && out.scalex.toFixed(9) == out.scaley.toFixed(9) && !out.rotate;
+ out.noRotation = !+out.shear.toFixed(9) && !out.rotate;
+ return out;
+ };
+ /*\
+ * Matrix.toTransformString
+ [ method ]
+ **
+ * Return transform string that represents given matrix
+ = (string) transform string
+ \*/
+ matrixproto.toTransformString = function (shorter) {
+ var s = shorter || this[split]();
+ if (s.isSimple) {
+ s.scalex = +s.scalex.toFixed(4);
+ s.scaley = +s.scaley.toFixed(4);
+ s.rotate = +s.rotate.toFixed(4);
+ return (s.dx || s.dy ? "t" + [s.dx, s.dy] : E) +
+ (s.scalex != 1 || s.scaley != 1 ? "s" + [s.scalex, s.scaley, 0, 0] : E) +
+ (s.rotate ? "r" + [s.rotate, 0, 0] : E);
+ } else {
+ return "m" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)];
+ }
+ };
+ })(Matrix.prototype);
+
+ // WebKit rendering bug workaround method
+ var version = navigator.userAgent.match(/Version\/(.*?)\s/) || navigator.userAgent.match(/Chrome\/(\d+)/);
+ if ((navigator.vendor == "Apple Computer, Inc.") && (version && version[1] < 4 || navigator.platform.slice(0, 2) == "iP") ||
+ (navigator.vendor == "Google Inc." && version && version[1] < 8)) {
+ /*\
+ * Paper.safari
+ [ method ]
+ **
+ * There is an inconvenient rendering bug in Safari (WebKit):
+ * sometimes the rendering should be forced.
+ * This method should help with dealing with this bug.
+ \*/
+ paperproto.safari = function () {
+ var rect = this.rect(-99, -99, this.width + 99, this.height + 99).attr({stroke: "none"});
+ setTimeout(function () {rect.remove();});
+ };
+ } else {
+ paperproto.safari = fun;
+ }
+
+ var preventDefault = function () {
+ this.returnValue = false;
+ },
+ preventTouch = function () {
+ return this.originalEvent.preventDefault();
+ },
+ stopPropagation = function () {
+ this.cancelBubble = true;
+ },
+ stopTouch = function () {
+ return this.originalEvent.stopPropagation();
+ },
+ getEventPosition = function (e) {
+ var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
+
+ return {
+ x: e.clientX + scrollX,
+ y: e.clientY + scrollY
+ };
+ },
+ addEvent = (function () {
+ if (g.doc.addEventListener) {
+ return function (obj, type, fn, element) {
+ var f = function (e) {
+ var pos = getEventPosition(e);
+ return fn.call(element, e, pos.x, pos.y);
+ };
+ obj.addEventListener(type, f, false);
+
+ if (supportsTouch && touchMap[type]) {
+ var _f = function (e) {
+ var pos = getEventPosition(e),
+ olde = e;
+
+ for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) {
+ if (e.targetTouches[i].target == obj) {
+ e = e.targetTouches[i];
+ e.originalEvent = olde;
+ e.preventDefault = preventTouch;
+ e.stopPropagation = stopTouch;
+ break;
+ }
+ }
+
+ return fn.call(element, e, pos.x, pos.y);
+ };
+ obj.addEventListener(touchMap[type], _f, false);
+ }
+
+ return function () {
+ obj.removeEventListener(type, f, false);
+
+ if (supportsTouch && touchMap[type])
+ obj.removeEventListener(touchMap[type], _f, false);
+
+ return true;
+ };
+ };
+ } else if (g.doc.attachEvent) {
+ return function (obj, type, fn, element) {
+ var f = function (e) {
+ e = e || g.win.event;
+ var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
+ x = e.clientX + scrollX,
+ y = e.clientY + scrollY;
+ e.preventDefault = e.preventDefault || preventDefault;
+ e.stopPropagation = e.stopPropagation || stopPropagation;
+ return fn.call(element, e, x, y);
+ };
+ obj.attachEvent("on" + type, f);
+ var detacher = function () {
+ obj.detachEvent("on" + type, f);
+ return true;
+ };
+ return detacher;
+ };
+ }
+ })(),
+ drag = [],
+ dragMove = function (e) {
+ var x = e.clientX,
+ y = e.clientY,
+ scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft,
+ dragi,
+ j = drag.length;
+ while (j--) {
+ dragi = drag[j];
+ if (supportsTouch && e.touches) {
+ var i = e.touches.length,
+ touch;
+ while (i--) {
+ touch = e.touches[i];
+ if (touch.identifier == dragi.el._drag.id) {
+ x = touch.clientX;
+ y = touch.clientY;
+ (e.originalEvent ? e.originalEvent : e).preventDefault();
+ break;
+ }
+ }
+ } else {
+ e.preventDefault();
+ }
+ var node = dragi.el.node,
+ o,
+ next = node.nextSibling,
+ parent = node.parentNode,
+ display = node.style.display;
+ g.win.opera && parent.removeChild(node);
+ node.style.display = "none";
+ o = dragi.el.paper.getElementByPoint(x, y);
+ node.style.display = display;
+ g.win.opera && (next ? parent.insertBefore(node, next) : parent.appendChild(node));
+ o && eve("raphael.drag.over." + dragi.el.id, dragi.el, o);
+ x += scrollX;
+ y += scrollY;
+ eve("raphael.drag.move." + dragi.el.id, dragi.move_scope || dragi.el, x - dragi.el._drag.x, y - dragi.el._drag.y, x, y, e);
+ }
+ },
+ dragUp = function (e) {
+ R.unmousemove(dragMove).unmouseup(dragUp);
+ var i = drag.length,
+ dragi;
+ while (i--) {
+ dragi = drag[i];
+ dragi.el._drag = {};
+ eve("raphael.drag.end." + dragi.el.id, dragi.end_scope || dragi.start_scope || dragi.move_scope || dragi.el, e);
+ }
+ drag = [];
+ },
+ /*\
+ * Raphael.el
+ [ property (object) ]
+ **
+ * You can add your own method to elements. This is usefull when you want to hack default functionality or
+ * want to wrap some common transformation or attributes in one method. In difference to canvas methods,
+ * you can redefine element method at any time. Expending element methods wouldn’t affect set.
+ > Usage
+ | Raphael.el.red = function () {
+ | this.attr({fill: "#f00"});
+ | };
+ | // then use it
+ | paper.circle(100, 100, 20).red();
+ \*/
+ elproto = R.el = {};
+ /*\
+ * Element.click
+ [ method ]
+ **
+ * Adds event handler for click for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unclick
+ [ method ]
+ **
+ * Removes event handler for click for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.dblclick
+ [ method ]
+ **
+ * Adds event handler for double click for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.undblclick
+ [ method ]
+ **
+ * Removes event handler for double click for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mousedown
+ [ method ]
+ **
+ * Adds event handler for mousedown for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmousedown
+ [ method ]
+ **
+ * Removes event handler for mousedown for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mousemove
+ [ method ]
+ **
+ * Adds event handler for mousemove for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmousemove
+ [ method ]
+ **
+ * Removes event handler for mousemove for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mouseout
+ [ method ]
+ **
+ * Adds event handler for mouseout for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmouseout
+ [ method ]
+ **
+ * Removes event handler for mouseout for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mouseover
+ [ method ]
+ **
+ * Adds event handler for mouseover for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmouseover
+ [ method ]
+ **
+ * Removes event handler for mouseover for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.mouseup
+ [ method ]
+ **
+ * Adds event handler for mouseup for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.unmouseup
+ [ method ]
+ **
+ * Removes event handler for mouseup for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchstart
+ [ method ]
+ **
+ * Adds event handler for touchstart for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchstart
+ [ method ]
+ **
+ * Removes event handler for touchstart for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchmove
+ [ method ]
+ **
+ * Adds event handler for touchmove for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchmove
+ [ method ]
+ **
+ * Removes event handler for touchmove for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchend
+ [ method ]
+ **
+ * Adds event handler for touchend for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchend
+ [ method ]
+ **
+ * Removes event handler for touchend for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+
+ /*\
+ * Element.touchcancel
+ [ method ]
+ **
+ * Adds event handler for touchcancel for the element.
+ > Parameters
+ - handler (function) handler for the event
+ = (object) @Element
+ \*/
+ /*\
+ * Element.untouchcancel
+ [ method ]
+ **
+ * Removes event handler for touchcancel for the element.
+ > Parameters
+ - handler (function) #optional handler for the event
+ = (object) @Element
+ \*/
+ for (var i = events.length; i--;) {
+ (function (eventName) {
+ R[eventName] = elproto[eventName] = function (fn, scope) {
+ if (R.is(fn, "function")) {
+ this.events = this.events || [];
+ this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || g.doc, eventName, fn, scope || this)});
+ }
+ return this;
+ };
+ R["un" + eventName] = elproto["un" + eventName] = function (fn) {
+ var events = this.events || [],
+ l = events.length;
+ while (l--){
+ if (events[l].name == eventName && (R.is(fn, "undefined") || events[l].f == fn)) {
+ events[l].unbind();
+ events.splice(l, 1);
+ !events.length && delete this.events;
+ }
+ }
+ return this;
+ };
+ })(events[i]);
+ }
+
+ /*\
+ * Element.data
+ [ method ]
+ **
+ * Adds or retrieves given value asociated with given key.
+ **
+ * See also @Element.removeData
+ > Parameters
+ - key (string) key to store data
+ - value (any) #optional value to store
+ = (object) @Element
+ * or, if value is not specified:
+ = (any) value
+ * or, if key and value are not specified:
+ = (object) Key/value pairs for all the data associated with the element.
+ > Usage
+ | for (var i = 0, i < 5, i++) {
+ | paper.circle(10 + 15 * i, 10, 10)
+ | .attr({fill: "#000"})
+ | .data("i", i)
+ | .click(function () {
+ | alert(this.data("i"));
+ | });
+ | }
+ \*/
+ elproto.data = function (key, value) {
+ var data = eldata[this.id] = eldata[this.id] || {};
+ if (arguments.length == 0) {
+ return data;
+ }
+ if (arguments.length == 1) {
+ if (R.is(key, "object")) {
+ for (var i in key) if (key[has](i)) {
+ this.data(i, key[i]);
+ }
+ return this;
+ }
+ eve("raphael.data.get." + this.id, this, data[key], key);
+ return data[key];
+ }
+ data[key] = value;
+ eve("raphael.data.set." + this.id, this, value, key);
+ return this;
+ };
+ /*\
+ * Element.removeData
+ [ method ]
+ **
+ * Removes value associated with an element by given key.
+ * If key is not provided, removes all the data of the element.
+ > Parameters
+ - key (string) #optional key
+ = (object) @Element
+ \*/
+ elproto.removeData = function (key) {
+ if (key == null) {
+ eldata[this.id] = {};
+ } else {
+ eldata[this.id] && delete eldata[this.id][key];
+ }
+ return this;
+ };
+ /*\
+ * Element.getData
+ [ method ]
+ **
+ * Retrieves the element data
+ = (object) data
+ \*/
+ elproto.getData = function () {
+ return clone(eldata[this.id] || {});
+ };
+ /*\
+ * Element.hover
+ [ method ]
+ **
+ * Adds event handlers for hover for the element.
+ > Parameters
+ - f_in (function) handler for hover in
+ - f_out (function) handler for hover out
+ - icontext (object) #optional context for hover in handler
+ - ocontext (object) #optional context for hover out handler
+ = (object) @Element
+ \*/
+ elproto.hover = function (f_in, f_out, scope_in, scope_out) {
+ return this.mouseover(f_in, scope_in).mouseout(f_out, scope_out || scope_in);
+ };
+ /*\
+ * Element.unhover
+ [ method ]
+ **
+ * Removes event handlers for hover for the element.
+ > Parameters
+ - f_in (function) handler for hover in
+ - f_out (function) handler for hover out
+ = (object) @Element
+ \*/
+ elproto.unhover = function (f_in, f_out) {
+ return this.unmouseover(f_in).unmouseout(f_out);
+ };
+ var draggable = [];
+ /*\
+ * Element.drag
+ [ method ]
+ **
+ * Adds event handlers for drag of the element.
+ > Parameters
+ - onmove (function) handler for moving
+ - onstart (function) handler for drag start
+ - onend (function) handler for drag end
+ - mcontext (object) #optional context for moving handler
+ - scontext (object) #optional context for drag start handler
+ - econtext (object) #optional context for drag end handler
+ * Additionaly following `drag` events will be triggered: `drag.start.<id>` on start,
+ * `drag.end.<id>` on end and `drag.move.<id>` on every move. When element will be dragged over another element
+ * `drag.over.<id>` will be fired as well.
+ *
+ * Start event and start handler will be called in specified context or in context of the element with following parameters:
+ o x (number) x position of the mouse
+ o y (number) y position of the mouse
+ o event (object) DOM event object
+ * Move event and move handler will be called in specified context or in context of the element with following parameters:
+ o dx (number) shift by x from the start point
+ o dy (number) shift by y from the start point
+ o x (number) x position of the mouse
+ o y (number) y position of the mouse
+ o event (object) DOM event object
+ * End event and end handler will be called in specified context or in context of the element with following parameters:
+ o event (object) DOM event object
+ = (object) @Element
+ \*/
+ elproto.drag = function (onmove, onstart, onend, move_scope, start_scope, end_scope) {
+ function start(e) {
+ (e.originalEvent || e).preventDefault();
+ var x = e.clientX,
+ y = e.clientY,
+ scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop,
+ scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft;
+ this._drag.id = e.identifier;
+ if (supportsTouch && e.touches) {
+ var i = e.touches.length, touch;
+ while (i--) {
+ touch = e.touches[i];
+ this._drag.id = touch.identifier;
+ if (touch.identifier == this._drag.id) {
+ x = touch.clientX;
+ y = touch.clientY;
+ break;
+ }
+ }
+ }
+ this._drag.x = x + scrollX;
+ this._drag.y = y + scrollY;
+ !drag.length && R.mousemove(dragMove).mouseup(dragUp);
+ drag.push({el: this, move_scope: move_scope, start_scope: start_scope, end_scope: end_scope});
+ onstart && eve.on("raphael.drag.start." + this.id, onstart);
+ onmove && eve.on("raphael.drag.move." + this.id, onmove);
+ onend && eve.on("raphael.drag.end." + this.id, onend);
+ eve("raphael.drag.start." + this.id, start_scope || move_scope || this, e.clientX + scrollX, e.clientY + scrollY, e);
+ }
+ this._drag = {};
+ draggable.push({el: this, start: start});
+ this.mousedown(start);
+ return this;
+ };
+ /*\
+ * Element.onDragOver
+ [ method ]
+ **
+ * Shortcut for assigning event handler for `drag.over.<id>` event, where id is id of the element (see @Element.id).
+ > Parameters
+ - f (function) handler for event, first argument would be the element you are dragging over
+ \*/
+ elproto.onDragOver = function (f) {
+ f ? eve.on("raphael.drag.over." + this.id, f) : eve.unbind("raphael.drag.over." + this.id);
+ };
+ /*\
+ * Element.undrag
+ [ method ]
+ **
+ * Removes all drag event handlers from given element.
+ \*/
+ elproto.undrag = function () {
+ var i = draggable.length;
+ while (i--) if (draggable[i].el == this) {
+ this.unmousedown(draggable[i].start);
+ draggable.splice(i, 1);
+ eve.unbind("raphael.drag.*." + this.id);
+ }
+ !draggable.length && R.unmousemove(dragMove).unmouseup(dragUp);
+ drag = [];
+ };
+ /*\
+ * Paper.circle
+ [ method ]
+ **
+ * Draws a circle.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the centre
+ - y (number) y coordinate of the centre
+ - r (number) radius
+ = (object) Raphaël element object with type “circle”
+ **
+ > Usage
+ | var c = paper.circle(50, 50, 40);
+ \*/
+ paperproto.circle = function (x, y, r) {
+ var out = R._engine.circle(this, x || 0, y || 0, r || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.rect
+ [ method ]
+ *
+ * Draws a rectangle.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the top left corner
+ - y (number) y coordinate of the top left corner
+ - width (number) width
+ - height (number) height
+ - r (number) #optional radius for rounded corners, default is 0
+ = (object) Raphaël element object with type “rect”
+ **
+ > Usage
+ | // regular rectangle
+ | var c = paper.rect(10, 10, 50, 50);
+ | // rectangle with rounded corners
+ | var c = paper.rect(40, 40, 50, 50, 10);
+ \*/
+ paperproto.rect = function (x, y, w, h, r) {
+ var out = R._engine.rect(this, x || 0, y || 0, w || 0, h || 0, r || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.ellipse
+ [ method ]
+ **
+ * Draws an ellipse.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the centre
+ - y (number) y coordinate of the centre
+ - rx (number) horizontal radius
+ - ry (number) vertical radius
+ = (object) Raphaël element object with type “ellipse”
+ **
+ > Usage
+ | var c = paper.ellipse(50, 50, 40, 20);
+ \*/
+ paperproto.ellipse = function (x, y, rx, ry) {
+ var out = R._engine.ellipse(this, x || 0, y || 0, rx || 0, ry || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.path
+ [ method ]
+ **
+ * Creates a path element by given path data string.
+ > Parameters
+ - pathString (string) #optional path string in SVG format.
+ * Path string consists of one-letter commands, followed by comma seprarated arguments in numercal form. Example:
+ | "M10,20L30,40"
+ * Here we can see two commands: “M”, with arguments `(10, 20)` and “L” with arguments `(30, 40)`. Upper case letter mean command is absolute, lower case—relative.
+ *
+ # <p>Here is short list of commands available, for more details see <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path's data attribute's format are described in the SVG specification.">SVG path string format</a>.</p>
+ # <table><thead><tr><th>Command</th><th>Name</th><th>Parameters</th></tr></thead><tbody>
+ # <tr><td>M</td><td>moveto</td><td>(x y)+</td></tr>
+ # <tr><td>Z</td><td>closepath</td><td>(none)</td></tr>
+ # <tr><td>L</td><td>lineto</td><td>(x y)+</td></tr>
+ # <tr><td>H</td><td>horizontal lineto</td><td>x+</td></tr>
+ # <tr><td>V</td><td>vertical lineto</td><td>y+</td></tr>
+ # <tr><td>C</td><td>curveto</td><td>(x1 y1 x2 y2 x y)+</td></tr>
+ # <tr><td>S</td><td>smooth curveto</td><td>(x2 y2 x y)+</td></tr>
+ # <tr><td>Q</td><td>quadratic Bézier curveto</td><td>(x1 y1 x y)+</td></tr>
+ # <tr><td>T</td><td>smooth quadratic Bézier curveto</td><td>(x y)+</td></tr>
+ # <tr><td>A</td><td>elliptical arc</td><td>(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+</td></tr>
+ # <tr><td>R</td><td><a href="http://en.wikipedia.org/wiki/Catmull–Rom_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom curveto</a>*</td><td>x1 y1 (x y)+</td></tr></tbody></table>
+ * * “Catmull-Rom curveto” is a not standard SVG command and added in 2.0 to make life easier.
+ * Note: there is a special case when path consist of just three commands: “M10,10R…z”. In this case path will smoothly connects to its beginning.
+ > Usage
+ | var c = paper.path("M10 10L90 90");
+ | // draw a diagonal line:
+ | // move to 10,10, line to 90,90
+ * For example of path strings, check out these icons: http://raphaeljs.com/icons/
+ \*/
+ paperproto.path = function (pathString) {
+ pathString && !R.is(pathString, string) && !R.is(pathString[0], array) && (pathString += E);
+ var out = R._engine.path(R.format[apply](R, arguments), this);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.image
+ [ method ]
+ **
+ * Embeds an image into the surface.
+ **
+ > Parameters
+ **
+ - src (string) URI of the source image
+ - x (number) x coordinate position
+ - y (number) y coordinate position
+ - width (number) width of the image
+ - height (number) height of the image
+ = (object) Raphaël element object with type “image”
+ **
+ > Usage
+ | var c = paper.image("apple.png", 10, 10, 80, 80);
+ \*/
+ paperproto.image = function (src, x, y, w, h) {
+ var out = R._engine.image(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0);
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.text
+ [ method ]
+ **
+ * Draws a text string. If you need line breaks, put “\n” in the string.
+ **
+ > Parameters
+ **
+ - x (number) x coordinate position
+ - y (number) y coordinate position
+ - text (string) The text string to draw
+ = (object) Raphaël element object with type “text”
+ **
+ > Usage
+ | var t = paper.text(50, 50, "Raphaël\nkicks\nbutt!");
+ \*/
+ paperproto.text = function (x, y, text) {
+ var out = R._engine.text(this, x || 0, y || 0, Str(text));
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Paper.set
+ [ method ]
+ **
+ * Creates array-like object to keep and operate several elements at once.
+ * Warning: it doesn’t create any elements for itself in the page, it just groups existing elements.
+ * Sets act as pseudo elements — all methods available to an element can be used on a set.
+ = (object) array-like object that represents set of elements
+ **
+ > Usage
+ | var st = paper.set();
+ | st.push(
+ | paper.circle(10, 10, 5),
+ | paper.circle(30, 10, 5)
+ | );
+ | st.attr({fill: "red"}); // changes the fill of both circles
+ \*/
+ paperproto.set = function (itemsArray) {
+ !R.is(itemsArray, "array") && (itemsArray = Array.prototype.splice.call(arguments, 0, arguments.length));
+ var out = new Set(itemsArray);
+ this.__set__ && this.__set__.push(out);
+ out["paper"] = this;
+ out["type"] = "set";
+ return out;
+ };
+ /*\
+ * Paper.setStart
+ [ method ]
+ **
+ * Creates @Paper.set. All elements that will be created after calling this method and before calling
+ * @Paper.setFinish will be added to the set.
+ **
+ > Usage
+ | paper.setStart();
+ | paper.circle(10, 10, 5),
+ | paper.circle(30, 10, 5)
+ | var st = paper.setFinish();
+ | st.attr({fill: "red"}); // changes the fill of both circles
+ \*/
+ paperproto.setStart = function (set) {
+ this.__set__ = set || this.set();
+ };
+ /*\
+ * Paper.setFinish
+ [ method ]
+ **
+ * See @Paper.setStart. This method finishes catching and returns resulting set.
+ **
+ = (object) set
+ \*/
+ paperproto.setFinish = function (set) {
+ var out = this.__set__;
+ delete this.__set__;
+ return out;
+ };
+ /*\
+ * Paper.getSize
+ [ method ]
+ **
+ * Obtains current paper actual size.
+ **
+ = (object)
+ \*/
+ paperproto.getSize = function () {
+ var container = this.canvas.parentNode;
+ return {
+ width: container.offsetWidth,
+ height: container.offsetHeight
+ };
+ };
+ /*\
+ * Paper.setSize
+ [ method ]
+ **
+ * If you need to change dimensions of the canvas call this method
+ **
+ > Parameters
+ **
+ - width (number) new width of the canvas
+ - height (number) new height of the canvas
+ \*/
+ paperproto.setSize = function (width, height) {
+ return R._engine.setSize.call(this, width, height);
+ };
+ /*\
+ * Paper.setViewBox
+ [ method ]
+ **
+ * Sets the view box of the paper. Practically it gives you ability to zoom and pan whole paper surface by
+ * specifying new boundaries.
+ **
+ > Parameters
+ **
+ - x (number) new x position, default is `0`
+ - y (number) new y position, default is `0`
+ - w (number) new width of the canvas
+ - h (number) new height of the canvas
+ - fit (boolean) `true` if you want graphics to fit into new boundary box
+ \*/
+ paperproto.setViewBox = function (x, y, w, h, fit) {
+ return R._engine.setViewBox.call(this, x, y, w, h, fit);
+ };
+ /*\
+ * Paper.top
+ [ property ]
+ **
+ * Points to the topmost element on the paper
+ \*/
+ /*\
+ * Paper.bottom
+ [ property ]
+ **
+ * Points to the bottom element on the paper
+ \*/
+ paperproto.top = paperproto.bottom = null;
+ /*\
+ * Paper.raphael
+ [ property ]
+ **
+ * Points to the @Raphael object/function
+ \*/
+ paperproto.raphael = R;
+ var getOffset = function (elem) {
+ var box = elem.getBoundingClientRect(),
+ doc = elem.ownerDocument,
+ body = doc.body,
+ docElem = doc.documentElement,
+ clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
+ top = box.top + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop,
+ left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft;
+ return {
+ y: top,
+ x: left
+ };
+ };
+ /*\
+ * Paper.getElementByPoint
+ [ method ]
+ **
+ * Returns you topmost element under given point.
+ **
+ = (object) Raphaël element object
+ > Parameters
+ **
+ - x (number) x coordinate from the top left corner of the window
+ - y (number) y coordinate from the top left corner of the window
+ > Usage
+ | paper.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"});
+ \*/
+ paperproto.getElementByPoint = function (x, y) {
+ var paper = this,
+ svg = paper.canvas,
+ target = g.doc.elementFromPoint(x, y);
+ if (g.win.opera && target.tagName == "svg") {
+ var so = getOffset(svg),
+ sr = svg.createSVGRect();
+ sr.x = x - so.x;
+ sr.y = y - so.y;
+ sr.width = sr.height = 1;
+ var hits = svg.getIntersectionList(sr, null);
+ if (hits.length) {
+ target = hits[hits.length - 1];
+ }
+ }
+ if (!target) {
+ return null;
+ }
+ while (target.parentNode && target != svg.parentNode && !target.raphael) {
+ target = target.parentNode;
+ }
+ target == paper.canvas.parentNode && (target = svg);
+ target = target && target.raphael ? paper.getById(target.raphaelid) : null;
+ return target;
+ };
+
+ /*\
+ * Paper.getElementsByBBox
+ [ method ]
+ **
+ * Returns set of elements that have an intersecting bounding box
+ **
+ > Parameters
+ **
+ - bbox (object) bbox to check with
+ = (object) @Set
+ \*/
+ paperproto.getElementsByBBox = function (bbox) {
+ var set = this.set();
+ this.forEach(function (el) {
+ if (R.isBBoxIntersect(el.getBBox(), bbox)) {
+ set.push(el);
+ }
+ });
+ return set;
+ };
+
+ /*\
+ * Paper.getById
+ [ method ]
+ **
+ * Returns you element by its internal ID.
+ **
+ > Parameters
+ **
+ - id (number) id
+ = (object) Raphaël element object
+ \*/
+ paperproto.getById = function (id) {
+ var bot = this.bottom;
+ while (bot) {
+ if (bot.id == id) {
+ return bot;
+ }
+ bot = bot.next;
+ }
+ return null;
+ };
+ /*\
+ * Paper.forEach
+ [ method ]
+ **
+ * Executes given function for each element on the paper
+ *
+ * If callback function returns `false` it will stop loop running.
+ **
+ > Parameters
+ **
+ - callback (function) function to run
+ - thisArg (object) context object for the callback
+ = (object) Paper object
+ > Usage
+ | paper.forEach(function (el) {
+ | el.attr({ stroke: "blue" });
+ | });
+ \*/
+ paperproto.forEach = function (callback, thisArg) {
+ var bot = this.bottom;
+ while (bot) {
+ if (callback.call(thisArg, bot) === false) {
+ return this;
+ }
+ bot = bot.next;
+ }
+ return this;
+ };
+ /*\
+ * Paper.getElementsByPoint
+ [ method ]
+ **
+ * Returns set of elements that have common point inside
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the point
+ - y (number) y coordinate of the point
+ = (object) @Set
+ \*/
+ paperproto.getElementsByPoint = function (x, y) {
+ var set = this.set();
+ this.forEach(function (el) {
+ if (el.isPointInside(x, y)) {
+ set.push(el);
+ }
+ });
+ return set;
+ };
+ function x_y() {
+ return this.x + S + this.y;
+ }
+ function x_y_w_h() {
+ return this.x + S + this.y + S + this.width + " \xd7 " + this.height;
+ }
+ /*\
+ * Element.isPointInside
+ [ method ]
+ **
+ * Determine if given point is inside this element’s shape
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the point
+ - y (number) y coordinate of the point
+ = (boolean) `true` if point inside the shape
+ \*/
+ elproto.isPointInside = function (x, y) {
+ var rp = this.realPath = getPath[this.type](this);
+ if (this.attr('transform') && this.attr('transform').length) {
+ rp = R.transformPath(rp, this.attr('transform'));
+ }
+ return R.isPointInsidePath(rp, x, y);
+ };
+ /*\
+ * Element.getBBox
+ [ method ]
+ **
+ * Return bounding box for a given element
+ **
+ > Parameters
+ **
+ - isWithoutTransform (boolean) flag, `true` if you want to have bounding box before transformations. Default is `false`.
+ = (object) Bounding box object:
+ o {
+ o x: (number) top left corner x
+ o y: (number) top left corner y
+ o x2: (number) bottom right corner x
+ o y2: (number) bottom right corner y
+ o width: (number) width
+ o height: (number) height
+ o }
+ \*/
+ elproto.getBBox = function (isWithoutTransform) {
+ if (this.removed) {
+ return {};
+ }
+ var _ = this._;
+ if (isWithoutTransform) {
+ if (_.dirty || !_.bboxwt) {
+ this.realPath = getPath[this.type](this);
+ _.bboxwt = pathDimensions(this.realPath);
+ _.bboxwt.toString = x_y_w_h;
+ _.dirty = 0;
+ }
+ return _.bboxwt;
+ }
+ if (_.dirty || _.dirtyT || !_.bbox) {
+ if (_.dirty || !this.realPath) {
+ _.bboxwt = 0;
+ this.realPath = getPath[this.type](this);
+ }
+ _.bbox = pathDimensions(mapPath(this.realPath, this.matrix));
+ _.bbox.toString = x_y_w_h;
+ _.dirty = _.dirtyT = 0;
+ }
+ return _.bbox;
+ };
+ /*\
+ * Element.clone
+ [ method ]
+ **
+ = (object) clone of a given element
+ **
+ \*/
+ elproto.clone = function () {
+ if (this.removed) {
+ return null;
+ }
+ var out = this.paper[this.type]().attr(this.attr());
+ this.__set__ && this.__set__.push(out);
+ return out;
+ };
+ /*\
+ * Element.glow
+ [ method ]
+ **
+ * Return set of elements that create glow-like effect around given element. See @Paper.set.
+ *
+ * Note: Glow is not connected to the element. If you change element attributes it won’t adjust itself.
+ **
+ > Parameters
+ **
+ - glow (object) #optional parameters object with all properties optional:
+ o {
+ o width (number) size of the glow, default is `10`
+ o fill (boolean) will it be filled, default is `false`
+ o opacity (number) opacity, default is `0.5`
+ o offsetx (number) horizontal offset, default is `0`
+ o offsety (number) vertical offset, default is `0`
+ o color (string) glow colour, default is `black`
+ o }
+ = (object) @Paper.set of elements that represents glow
+ \*/
+ elproto.glow = function (glow) {
+ if (this.type == "text") {
+ return null;
+ }
+ glow = glow || {};
+ var s = {
+ width: (glow.width || 10) + (+this.attr("stroke-width") || 1),
+ fill: glow.fill || false,
+ opacity: glow.opacity || .5,
+ offsetx: glow.offsetx || 0,
+ offsety: glow.offsety || 0,
+ color: glow.color || "#000"
+ },
+ c = s.width / 2,
+ r = this.paper,
+ out = r.set(),
+ path = this.realPath || getPath[this.type](this);
+ path = this.matrix ? mapPath(path, this.matrix) : path;
+ for (var i = 1; i < c + 1; i++) {
+ out.push(r.path(path).attr({
+ stroke: s.color,
+ fill: s.fill ? s.color : "none",
+ "stroke-linejoin": "round",
+ "stroke-linecap": "round",
+ "stroke-width": +(s.width / c * i).toFixed(3),
+ opacity: +(s.opacity / c).toFixed(3)
+ }));
+ }
+ return out.insertBefore(this).translate(s.offsetx, s.offsety);
+ };
+ var curveslengths = {},
+ getPointAtSegmentLength = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) {
+ if (length == null) {
+ return bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
+ } else {
+ return R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length));
+ }
+ },
+ getLengthFactory = function (istotal, subpath) {
+ return function (path, length, onlystart) {
+ path = path2curve(path);
+ var x, y, p, l, sp = "", subpaths = {}, point,
+ len = 0;
+ for (var i = 0, ii = path.length; i < ii; i++) {
+ p = path[i];
+ if (p[0] == "M") {
+ x = +p[1];
+ y = +p[2];
+ } else {
+ l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+ if (len + l > length) {
+ if (subpath && !subpaths.start) {
+ point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
+ sp += ["C" + point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y];
+ if (onlystart) {return sp;}
+ subpaths.start = sp;
+ sp = ["M" + point.x, point.y + "C" + point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]].join();
+ len += l;
+ x = +p[5];
+ y = +p[6];
+ continue;
+ }
+ if (!istotal && !subpath) {
+ point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len);
+ return {x: point.x, y: point.y, alpha: point.alpha};
+ }
+ }
+ len += l;
+ x = +p[5];
+ y = +p[6];
+ }
+ sp += p.shift() + p;
+ }
+ subpaths.end = sp;
+ point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1);
+ point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha});
+ return point;
+ };
+ };
+ var getTotalLength = getLengthFactory(1),
+ getPointAtLength = getLengthFactory(),
+ getSubpathsAtLength = getLengthFactory(0, 1);
+ /*\
+ * Raphael.getTotalLength
+ [ method ]
+ **
+ * Returns length of the given path in pixels.
+ **
+ > Parameters
+ **
+ - path (string) SVG path string.
+ **
+ = (number) length.
+ \*/
+ R.getTotalLength = getTotalLength;
+ /*\
+ * Raphael.getPointAtLength
+ [ method ]
+ **
+ * Return coordinates of the point located at the given length on the given path.
+ **
+ > Parameters
+ **
+ - path (string) SVG path string
+ - length (number)
+ **
+ = (object) representation of the point:
+ o {
+ o x: (number) x coordinate
+ o y: (number) y coordinate
+ o alpha: (number) angle of derivative
+ o }
+ \*/
+ R.getPointAtLength = getPointAtLength;
+ /*\
+ * Raphael.getSubpath
+ [ method ]
+ **
+ * Return subpath of a given path from given length to given length.
+ **
+ > Parameters
+ **
+ - path (string) SVG path string
+ - from (number) position of the start of the segment
+ - to (number) position of the end of the segment
+ **
+ = (string) pathstring for the segment
+ \*/
+ R.getSubpath = function (path, from, to) {
+ if (this.getTotalLength(path) - to < 1e-6) {
+ return getSubpathsAtLength(path, from).end;
+ }
+ var a = getSubpathsAtLength(path, to, 1);
+ return from ? getSubpathsAtLength(a, from).end : a;
+ };
+ /*\
+ * Element.getTotalLength
+ [ method ]
+ **
+ * Returns length of the path in pixels. Only works for element of “path” type.
+ = (number) length.
+ \*/
+ elproto.getTotalLength = function () {
+ var path = this.getPath();
+ if (!path) {
+ return;
+ }
+
+ if (this.node.getTotalLength) {
+ return this.node.getTotalLength();
+ }
+
+ return getTotalLength(path);
+ };
+ /*\
+ * Element.getPointAtLength
+ [ method ]
+ **
+ * Return coordinates of the point located at the given length on the given path. Only works for element of “path” type.
+ **
+ > Parameters
+ **
+ - length (number)
+ **
+ = (object) representation of the point:
+ o {
+ o x: (number) x coordinate
+ o y: (number) y coordinate
+ o alpha: (number) angle of derivative
+ o }
+ \*/
+ elproto.getPointAtLength = function (length) {
+ var path = this.getPath();
+ if (!path) {
+ return;
+ }
+
+ return getPointAtLength(path, length);
+ };
+ /*\
+ * Element.getPath
+ [ method ]
+ **
+ * Returns path of the element. Only works for elements of “path” type and simple elements like circle.
+ = (object) path
+ **
+ \*/
+ elproto.getPath = function () {
+ var path,
+ getPath = R._getPath[this.type];
+
+ if (this.type == "text" || this.type == "set") {
+ return;
+ }
+
+ if (getPath) {
+ path = getPath(this);
+ }
+
+ return path;
+ };
+ /*\
+ * Element.getSubpath
+ [ method ]
+ **
+ * Return subpath of a given element from given length to given length. Only works for element of “path” type.
+ **
+ > Parameters
+ **
+ - from (number) position of the start of the segment
+ - to (number) position of the end of the segment
+ **
+ = (string) pathstring for the segment
+ \*/
+ elproto.getSubpath = function (from, to) {
+ var path = this.getPath();
+ if (!path) {
+ return;
+ }
+
+ return R.getSubpath(path, from, to);
+ };
+ /*\
+ * Raphael.easing_formulas
+ [ property ]
+ **
+ * Object that contains easing formulas for animation. You could extend it with your own. By default it has following list of easing:
+ # <ul>
+ # <li>“linear”</li>
+ # <li>“&lt;” or “easeIn” or “ease-in”</li>
+ # <li>“>” or “easeOut” or “ease-out”</li>
+ # <li>“&lt;>” or “easeInOut” or “ease-in-out”</li>
+ # <li>“backIn” or “back-in”</li>
+ # <li>“backOut” or “back-out”</li>
+ # <li>“elastic”</li>
+ # <li>“bounce”</li>
+ # </ul>
+ # <p>See also <a href="http://raphaeljs.com/easing.html">Easing demo</a>.</p>
+ \*/
+ var ef = R.easing_formulas = {
+ linear: function (n) {
+ return n;
+ },
+ "<": function (n) {
+ return pow(n, 1.7);
+ },
+ ">": function (n) {
+ return pow(n, .48);
+ },
+ "<>": function (n) {
+ var q = .48 - n / 1.04,
+ Q = math.sqrt(.1734 + q * q),
+ x = Q - q,
+ X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1),
+ y = -Q - q,
+ Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1),
+ t = X + Y + .5;
+ return (1 - t) * 3 * t * t + t * t * t;
+ },
+ backIn: function (n) {
+ var s = 1.70158;
+ return n * n * ((s + 1) * n - s);
+ },
+ backOut: function (n) {
+ n = n - 1;
+ var s = 1.70158;
+ return n * n * ((s + 1) * n + s) + 1;
+ },
+ elastic: function (n) {
+ if (n == !!n) {
+ return n;
+ }
+ return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1;
+ },
+ bounce: function (n) {
+ var s = 7.5625,
+ p = 2.75,
+ l;
+ if (n < (1 / p)) {
+ l = s * n * n;
+ } else {
+ if (n < (2 / p)) {
+ n -= (1.5 / p);
+ l = s * n * n + .75;
+ } else {
+ if (n < (2.5 / p)) {
+ n -= (2.25 / p);
+ l = s * n * n + .9375;
+ } else {
+ n -= (2.625 / p);
+ l = s * n * n + .984375;
+ }
+ }
+ }
+ return l;
+ }
+ };
+ ef.easeIn = ef["ease-in"] = ef["<"];
+ ef.easeOut = ef["ease-out"] = ef[">"];
+ ef.easeInOut = ef["ease-in-out"] = ef["<>"];
+ ef["back-in"] = ef.backIn;
+ ef["back-out"] = ef.backOut;
+
+ var animationElements = [],
+ requestAnimFrame = window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function (callback) {
+ setTimeout(callback, 16);
+ },
+ animation = function () {
+ var Now = +new Date,
+ l = 0;
+ for (; l < animationElements.length; l++) {
+ var e = animationElements[l];
+ if (e.el.removed || e.paused) {
+ continue;
+ }
+ var time = Now - e.start,
+ ms = e.ms,
+ easing = e.easing,
+ from = e.from,
+ diff = e.diff,
+ to = e.to,
+ t = e.t,
+ that = e.el,
+ set = {},
+ now,
+ init = {},
+ key;
+ if (e.initstatus) {
+ time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms;
+ e.status = e.initstatus;
+ delete e.initstatus;
+ e.stop && animationElements.splice(l--, 1);
+ } else {
+ e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top;
+ }
+ if (time < 0) {
+ continue;
+ }
+ if (time < ms) {
+ var pos = easing(time / ms);
+ for (var attr in from) if (from[has](attr)) {
+ switch (availableAnimAttrs[attr]) {
+ case nu:
+ now = +from[attr] + pos * ms * diff[attr];
+ break;
+ case "colour":
+ now = "rgb(" + [
+ upto255(round(from[attr].r + pos * ms * diff[attr].r)),
+ upto255(round(from[attr].g + pos * ms * diff[attr].g)),
+ upto255(round(from[attr].b + pos * ms * diff[attr].b))
+ ].join(",") + ")";
+ break;
+ case "path":
+ now = [];
+ for (var i = 0, ii = from[attr].length; i < ii; i++) {
+ now[i] = [from[attr][i][0]];
+ for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
+ now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j];
+ }
+ now[i] = now[i].join(S);
+ }
+ now = now.join(S);
+ break;
+ case "transform":
+ if (diff[attr].real) {
+ now = [];
+ for (i = 0, ii = from[attr].length; i < ii; i++) {
+ now[i] = [from[attr][i][0]];
+ for (j = 1, jj = from[attr][i].length; j < jj; j++) {
+ now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j];
+ }
+ }
+ } else {
+ var get = function (i) {
+ return +from[attr][i] + pos * ms * diff[attr][i];
+ };
+ // now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]];
+ now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]];
+ }
+ break;
+ case "csv":
+ if (attr == "clip-rect") {
+ now = [];
+ i = 4;
+ while (i--) {
+ now[i] = +from[attr][i] + pos * ms * diff[attr][i];
+ }
+ }
+ break;
+ default:
+ var from2 = [][concat](from[attr]);
+ now = [];
+ i = that.paper.customAttributes[attr].length;
+ while (i--) {
+ now[i] = +from2[i] + pos * ms * diff[attr][i];
+ }
+ break;
+ }
+ set[attr] = now;
+ }
+ that.attr(set);
+ (function (id, that, anim) {
+ setTimeout(function () {
+ eve("raphael.anim.frame." + id, that, anim);
+ });
+ })(that.id, that, e.anim);
+ } else {
+ (function(f, el, a) {
+ setTimeout(function() {
+ eve("raphael.anim.frame." + el.id, el, a);
+ eve("raphael.anim.finish." + el.id, el, a);
+ R.is(f, "function") && f.call(el);
+ });
+ })(e.callback, that, e.anim);
+ that.attr(to);
+ animationElements.splice(l--, 1);
+ if (e.repeat > 1 && !e.next) {
+ for (key in to) if (to[has](key)) {
+ init[key] = e.totalOrigin[key];
+ }
+ e.el.attr(init);
+ runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1);
+ }
+ if (e.next && !e.stop) {
+ runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat);
+ }
+ }
+ }
+ R.svg && that && that.paper && that.paper.safari();
+ animationElements.length && requestAnimFrame(animation);
+ },
+ upto255 = function (color) {
+ return color > 255 ? 255 : color < 0 ? 0 : color;
+ };
+ /*\
+ * Element.animateWith
+ [ method ]
+ **
+ * Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element.
+ **
+ > Parameters
+ **
+ - el (object) element to sync with
+ - anim (object) animation to sync with
+ - params (object) #optional final attributes for the element, see also @Element.attr
+ - ms (number) #optional number of milliseconds for animation to run
+ - easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
+ - callback (function) #optional callback function. Will be called at the end of animation.
+ * or
+ - element (object) element to sync with
+ - anim (object) animation to sync with
+ - animation (object) #optional animation object, see @Raphael.animation
+ **
+ = (object) original element
+ \*/
+ elproto.animateWith = function (el, anim, params, ms, easing, callback) {
+ var element = this;
+ if (element.removed) {
+ callback && callback.call(element);
+ return element;
+ }
+ var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback),
+ x, y;
+ runAnimation(a, element, a.percents[0], null, element.attr());
+ for (var i = 0, ii = animationElements.length; i < ii; i++) {
+ if (animationElements[i].anim == anim && animationElements[i].el == el) {
+ animationElements[ii - 1].start = animationElements[i].start;
+ break;
+ }
+ }
+ return element;
+ //
+ //
+ // var a = params ? R.animation(params, ms, easing, callback) : anim,
+ // status = element.status(anim);
+ // return this.animate(a).status(a, status * anim.ms / a.ms);
+ };
+ function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) {
+ var cx = 3 * p1x,
+ bx = 3 * (p2x - p1x) - cx,
+ ax = 1 - cx - bx,
+ cy = 3 * p1y,
+ by = 3 * (p2y - p1y) - cy,
+ ay = 1 - cy - by;
+ function sampleCurveX(t) {
+ return ((ax * t + bx) * t + cx) * t;
+ }
+ function solve(x, epsilon) {
+ var t = solveCurveX(x, epsilon);
+ return ((ay * t + by) * t + cy) * t;
+ }
+ function solveCurveX(x, epsilon) {
+ var t0, t1, t2, x2, d2, i;
+ for(t2 = x, i = 0; i < 8; i++) {
+ x2 = sampleCurveX(t2) - x;
+ if (abs(x2) < epsilon) {
+ return t2;
+ }
+ d2 = (3 * ax * t2 + 2 * bx) * t2 + cx;
+ if (abs(d2) < 1e-6) {
+ break;
+ }
+ t2 = t2 - x2 / d2;
+ }
+ t0 = 0;
+ t1 = 1;
+ t2 = x;
+ if (t2 < t0) {
+ return t0;
+ }
+ if (t2 > t1) {
+ return t1;
+ }
+ while (t0 < t1) {
+ x2 = sampleCurveX(t2);
+ if (abs(x2 - x) < epsilon) {
+ return t2;
+ }
+ if (x > x2) {
+ t0 = t2;
+ } else {
+ t1 = t2;
+ }
+ t2 = (t1 - t0) / 2 + t0;
+ }
+ return t2;
+ }
+ return solve(t, 1 / (200 * duration));
+ }
+ elproto.onAnimation = function (f) {
+ f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id);
+ return this;
+ };
+ function Animation(anim, ms) {
+ var percents = [],
+ newAnim = {};
+ this.ms = ms;
+ this.times = 1;
+ if (anim) {
+ for (var attr in anim) if (anim[has](attr)) {
+ newAnim[toFloat(attr)] = anim[attr];
+ percents.push(toFloat(attr));
+ }
+ percents.sort(sortByNumber);
+ }
+ this.anim = newAnim;
+ this.top = percents[percents.length - 1];
+ this.percents = percents;
+ }
+ /*\
+ * Animation.delay
+ [ method ]
+ **
+ * Creates a copy of existing animation object with given delay.
+ **
+ > Parameters
+ **
+ - delay (number) number of ms to pass between animation start and actual animation
+ **
+ = (object) new altered Animation object
+ | var anim = Raphael.animation({cx: 10, cy: 20}, 2e3);
+ | circle1.animate(anim); // run the given animation immediately
+ | circle2.animate(anim.delay(500)); // run the given animation after 500 ms
+ \*/
+ Animation.prototype.delay = function (delay) {
+ var a = new Animation(this.anim, this.ms);
+ a.times = this.times;
+ a.del = +delay || 0;
+ return a;
+ };
+ /*\
+ * Animation.repeat
+ [ method ]
+ **
+ * Creates a copy of existing animation object with given repetition.
+ **
+ > Parameters
+ **
+ - repeat (number) number iterations of animation. For infinite animation pass `Infinity`
+ **
+ = (object) new altered Animation object
+ \*/
+ Animation.prototype.repeat = function (times) {
+ var a = new Animation(this.anim, this.ms);
+ a.del = this.del;
+ a.times = math.floor(mmax(times, 0)) || 1;
+ return a;
+ };
+ function runAnimation(anim, element, percent, status, totalOrigin, times) {
+ percent = toFloat(percent);
+ var params,
+ isInAnim,
+ isInAnimSet,
+ percents = [],
+ next,
+ prev,
+ timestamp,
+ ms = anim.ms,
+ from = {},
+ to = {},
+ diff = {};
+ if (status) {
+ for (i = 0, ii = animationElements.length; i < ii; i++) {
+ var e = animationElements[i];
+ if (e.el.id == element.id && e.anim == anim) {
+ if (e.percent != percent) {
+ animationElements.splice(i, 1);
+ isInAnimSet = 1;
+ } else {
+ isInAnim = e;
+ }
+ element.attr(e.totalOrigin);
+ break;
+ }
+ }
+ } else {
+ status = +to; // NaN
+ }
+ for (var i = 0, ii = anim.percents.length; i < ii; i++) {
+ if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) {
+ percent = anim.percents[i];
+ prev = anim.percents[i - 1] || 0;
+ ms = ms / anim.top * (percent - prev);
+ next = anim.percents[i + 1];
+ params = anim.anim[percent];
+ break;
+ } else if (status) {
+ element.attr(anim.anim[anim.percents[i]]);
+ }
+ }
+ if (!params) {
+ return;
+ }
+ if (!isInAnim) {
+ for (var attr in params) if (params[has](attr)) {
+ if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) {
+ from[attr] = element.attr(attr);
+ (from[attr] == null) && (from[attr] = availableAttrs[attr]);
+ to[attr] = params[attr];
+ switch (availableAnimAttrs[attr]) {
+ case nu:
+ diff[attr] = (to[attr] - from[attr]) / ms;
+ break;
+ case "colour":
+ from[attr] = R.getRGB(from[attr]);
+ var toColour = R.getRGB(to[attr]);
+ diff[attr] = {
+ r: (toColour.r - from[attr].r) / ms,
+ g: (toColour.g - from[attr].g) / ms,
+ b: (toColour.b - from[attr].b) / ms
+ };
+ break;
+ case "path":
+ var pathes = path2curve(from[attr], to[attr]),
+ toPath = pathes[1];
+ from[attr] = pathes[0];
+ diff[attr] = [];
+ for (i = 0, ii = from[attr].length; i < ii; i++) {
+ diff[attr][i] = [0];
+ for (var j = 1, jj = from[attr][i].length; j < jj; j++) {
+ diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms;
+ }
+ }
+ break;
+ case "transform":
+ var _ = element._,
+ eq = equaliseTransform(_[attr], to[attr]);
+ if (eq) {
+ from[attr] = eq.from;
+ to[attr] = eq.to;
+ diff[attr] = [];
+ diff[attr].real = true;
+ for (i = 0, ii = from[attr].length; i < ii; i++) {
+ diff[attr][i] = [from[attr][i][0]];
+ for (j = 1, jj = from[attr][i].length; j < jj; j++) {
+ diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms;
+ }
+ }
+ } else {
+ var m = (element.matrix || new Matrix),
+ to2 = {
+ _: {transform: _.transform},
+ getBBox: function () {
+ return element.getBBox(1);
+ }
+ };
+ from[attr] = [
+ m.a,
+ m.b,
+ m.c,
+ m.d,
+ m.e,
+ m.f
+ ];
+ extractTransform(to2, to[attr]);
+ to[attr] = to2._.transform;
+ diff[attr] = [
+ (to2.matrix.a - m.a) / ms,
+ (to2.matrix.b - m.b) / ms,
+ (to2.matrix.c - m.c) / ms,
+ (to2.matrix.d - m.d) / ms,
+ (to2.matrix.e - m.e) / ms,
+ (to2.matrix.f - m.f) / ms
+ ];
+ // from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy];
+ // var to2 = {_:{}, getBBox: function () { return element.getBBox(); }};
+ // extractTransform(to2, to[attr]);
+ // diff[attr] = [
+ // (to2._.sx - _.sx) / ms,
+ // (to2._.sy - _.sy) / ms,
+ // (to2._.deg - _.deg) / ms,
+ // (to2._.dx - _.dx) / ms,
+ // (to2._.dy - _.dy) / ms
+ // ];
+ }
+ break;
+ case "csv":
+ var values = Str(params[attr])[split](separator),
+ from2 = Str(from[attr])[split](separator);
+ if (attr == "clip-rect") {
+ from[attr] = from2;
+ diff[attr] = [];
+ i = from2.length;
+ while (i--) {
+ diff[attr][i] = (values[i] - from[attr][i]) / ms;
+ }
+ }
+ to[attr] = values;
+ break;
+ default:
+ values = [][concat](params[attr]);
+ from2 = [][concat](from[attr]);
+ diff[attr] = [];
+ i = element.paper.customAttributes[attr].length;
+ while (i--) {
+ diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms;
+ }
+ break;
+ }
+ }
+ }
+ var easing = params.easing,
+ easyeasy = R.easing_formulas[easing];
+ if (!easyeasy) {
+ easyeasy = Str(easing).match(bezierrg);
+ if (easyeasy && easyeasy.length == 5) {
+ var curve = easyeasy;
+ easyeasy = function (t) {
+ return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms);
+ };
+ } else {
+ easyeasy = pipe;
+ }
+ }
+ timestamp = params.start || anim.start || +new Date;
+ e = {
+ anim: anim,
+ percent: percent,
+ timestamp: timestamp,
+ start: timestamp + (anim.del || 0),
+ status: 0,
+ initstatus: status || 0,
+ stop: false,
+ ms: ms,
+ easing: easyeasy,
+ from: from,
+ diff: diff,
+ to: to,
+ el: element,
+ callback: params.callback,
+ prev: prev,
+ next: next,
+ repeat: times || anim.times,
+ origin: element.attr(),
+ totalOrigin: totalOrigin
+ };
+ animationElements.push(e);
+ if (status && !isInAnim && !isInAnimSet) {
+ e.stop = true;
+ e.start = new Date - ms * status;
+ if (animationElements.length == 1) {
+ return animation();
+ }
+ }
+ if (isInAnimSet) {
+ e.start = new Date - e.ms * status;
+ }
+ animationElements.length == 1 && requestAnimFrame(animation);
+ } else {
+ isInAnim.initstatus = status;
+ isInAnim.start = new Date - isInAnim.ms * status;
+ }
+ eve("raphael.anim.start." + element.id, element, anim);
+ }
+ /*\
+ * Raphael.animation
+ [ method ]
+ **
+ * Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods.
+ * See also @Animation.delay and @Animation.repeat methods.
+ **
+ > Parameters
+ **
+ - params (object) final attributes for the element, see also @Element.attr
+ - ms (number) number of milliseconds for animation to run
+ - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
+ - callback (function) #optional callback function. Will be called at the end of animation.
+ **
+ = (object) @Animation
+ \*/
+ R.animation = function (params, ms, easing, callback) {
+ if (params instanceof Animation) {
+ return params;
+ }
+ if (R.is(easing, "function") || !easing) {
+ callback = callback || easing || null;
+ easing = null;
+ }
+ params = Object(params);
+ ms = +ms || 0;
+ var p = {},
+ json,
+ attr;
+ for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) {
+ json = true;
+ p[attr] = params[attr];
+ }
+ if (!json) {
+ // if percent-like syntax is used and end-of-all animation callback used
+ if(callback){
+ // find the last one
+ var lastKey = 0;
+ for(var i in params){
+ var percent = toInt(i);
+ if(params[has](i) && percent > lastKey){
+ lastKey = percent;
+ }
+ }
+ lastKey += '%';
+ // if already defined callback in the last keyframe, skip
+ !params[lastKey].callback && (params[lastKey].callback = callback);
+ }
+ return new Animation(params, ms);
+ } else {
+ easing && (p.easing = easing);
+ callback && (p.callback = callback);
+ return new Animation({100: p}, ms);
+ }
+ };
+ /*\
+ * Element.animate
+ [ method ]
+ **
+ * Creates and starts animation for given element.
+ **
+ > Parameters
+ **
+ - params (object) final attributes for the element, see also @Element.attr
+ - ms (number) number of milliseconds for animation to run
+ - easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic&#x2010;bezier(XX,&#160;XX,&#160;XX,&#160;XX)`
+ - callback (function) #optional callback function. Will be called at the end of animation.
+ * or
+ - animation (object) animation object, see @Raphael.animation
+ **
+ = (object) original element
+ \*/
+ elproto.animate = function (params, ms, easing, callback) {
+ var element = this;
+ if (element.removed) {
+ callback && callback.call(element);
+ return element;
+ }
+ var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback);
+ runAnimation(anim, element, anim.percents[0], null, element.attr());
+ return element;
+ };
+ /*\
+ * Element.setTime
+ [ method ]
+ **
+ * Sets the status of animation of the element in milliseconds. Similar to @Element.status method.
+ **
+ > Parameters
+ **
+ - anim (object) animation object
+ - value (number) number of milliseconds from the beginning of the animation
+ **
+ = (object) original element if `value` is specified
+ * Note, that during animation following events are triggered:
+ *
+ * On each animation frame event `anim.frame.<id>`, on start `anim.start.<id>` and on end `anim.finish.<id>`.
+ \*/
+ elproto.setTime = function (anim, value) {
+ if (anim && value != null) {
+ this.status(anim, mmin(value, anim.ms) / anim.ms);
+ }
+ return this;
+ };
+ /*\
+ * Element.status
+ [ method ]
+ **
+ * Gets or sets the status of animation of the element.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ - value (number) #optional 0 – 1. If specified, method works like a setter and sets the status of a given animation to the value. This will cause animation to jump to the given position.
+ **
+ = (number) status
+ * or
+ = (array) status if `anim` is not specified. Array of objects in format:
+ o {
+ o anim: (object) animation object
+ o status: (number) status
+ o }
+ * or
+ = (object) original element if `value` is specified
+ \*/
+ elproto.status = function (anim, value) {
+ var out = [],
+ i = 0,
+ len,
+ e;
+ if (value != null) {
+ runAnimation(anim, this, -1, mmin(value, 1));
+ return this;
+ } else {
+ len = animationElements.length;
+ for (; i < len; i++) {
+ e = animationElements[i];
+ if (e.el.id == this.id && (!anim || e.anim == anim)) {
+ if (anim) {
+ return e.status;
+ }
+ out.push({
+ anim: e.anim,
+ status: e.status
+ });
+ }
+ }
+ if (anim) {
+ return 0;
+ }
+ return out;
+ }
+ };
+ /*\
+ * Element.pause
+ [ method ]
+ **
+ * Stops animation of the element with ability to resume it later on.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ **
+ = (object) original element
+ \*/
+ elproto.pause = function (anim) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
+ if (eve("raphael.anim.pause." + this.id, this, animationElements[i].anim) !== false) {
+ animationElements[i].paused = true;
+ }
+ }
+ return this;
+ };
+ /*\
+ * Element.resume
+ [ method ]
+ **
+ * Resumes animation if it was paused with @Element.pause method.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ **
+ = (object) original element
+ \*/
+ elproto.resume = function (anim) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
+ var e = animationElements[i];
+ if (eve("raphael.anim.resume." + this.id, this, e.anim) !== false) {
+ delete e.paused;
+ this.status(e.anim, e.status);
+ }
+ }
+ return this;
+ };
+ /*\
+ * Element.stop
+ [ method ]
+ **
+ * Stops animation of the element.
+ **
+ > Parameters
+ **
+ - anim (object) #optional animation object
+ **
+ = (object) original element
+ \*/
+ elproto.stop = function (anim) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) {
+ if (eve("raphael.anim.stop." + this.id, this, animationElements[i].anim) !== false) {
+ animationElements.splice(i--, 1);
+ }
+ }
+ return this;
+ };
+ function stopAnimation(paper) {
+ for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.paper == paper) {
+ animationElements.splice(i--, 1);
+ }
+ }
+ eve.on("raphael.remove", stopAnimation);
+ eve.on("raphael.clear", stopAnimation);
+ elproto.toString = function () {
+ return "Rapha\xebl\u2019s object";
+ };
+
+ // Set
+ var Set = function (items) {
+ this.items = [];
+ this.length = 0;
+ this.type = "set";
+ if (items) {
+ for (var i = 0, ii = items.length; i < ii; i++) {
+ if (items[i] && (items[i].constructor == elproto.constructor || items[i].constructor == Set)) {
+ this[this.items.length] = this.items[this.items.length] = items[i];
+ this.length++;
+ }
+ }
+ }
+ },
+ setproto = Set.prototype;
+ /*\
+ * Set.push
+ [ method ]
+ **
+ * Adds each argument to the current set.
+ = (object) original element
+ \*/
+ setproto.push = function () {
+ var item,
+ len;
+ for (var i = 0, ii = arguments.length; i < ii; i++) {
+ item = arguments[i];
+ if (item && (item.constructor == elproto.constructor || item.constructor == Set)) {
+ len = this.items.length;
+ this[len] = this.items[len] = item;
+ this.length++;
+ }
+ }
+ return this;
+ };
+ /*\
+ * Set.pop
+ [ method ]
+ **
+ * Removes last element and returns it.
+ = (object) element
+ \*/
+ setproto.pop = function () {
+ this.length && delete this[this.length--];
+ return this.items.pop();
+ };
+ /*\
+ * Set.forEach
+ [ method ]
+ **
+ * Executes given function for each element in the set.
+ *
+ * If function returns `false` it will stop loop running.
+ **
+ > Parameters
+ **
+ - callback (function) function to run
+ - thisArg (object) context object for the callback
+ = (object) Set object
+ \*/
+ setproto.forEach = function (callback, thisArg) {
+ for (var i = 0, ii = this.items.length; i < ii; i++) {
+ if (callback.call(thisArg, this.items[i], i) === false) {
+ return this;
+ }
+ }
+ return this;
+ };
+ for (var method in elproto) if (elproto[has](method)) {
+ setproto[method] = (function (methodname) {
+ return function () {
+ var arg = arguments;
+ return this.forEach(function (el) {
+ el[methodname][apply](el, arg);
+ });
+ };
+ })(method);
+ }
+ setproto.attr = function (name, value) {
+ if (name && R.is(name, array) && R.is(name[0], "object")) {
+ for (var j = 0, jj = name.length; j < jj; j++) {
+ this.items[j].attr(name[j]);
+ }
+ } else {
+ for (var i = 0, ii = this.items.length; i < ii; i++) {
+ this.items[i].attr(name, value);
+ }
+ }
+ return this;
+ };
+ /*\
+ * Set.clear
+ [ method ]
+ **
+ * Removes all elements from the set
+ \*/
+ setproto.clear = function () {
+ while (this.length) {
+ this.pop();
+ }
+ };
+ /*\
+ * Set.splice
+ [ method ]
+ **
+ * Removes given element from the set
+ **
+ > Parameters
+ **
+ - index (number) position of the deletion
+ - count (number) number of element to remove
+ - insertion… (object) #optional elements to insert
+ = (object) set elements that were deleted
+ \*/
+ setproto.splice = function (index, count, insertion) {
+ index = index < 0 ? mmax(this.length + index, 0) : index;
+ count = mmax(0, mmin(this.length - index, count));
+ var tail = [],
+ todel = [],
+ args = [],
+ i;
+ for (i = 2; i < arguments.length; i++) {
+ args.push(arguments[i]);
+ }
+ for (i = 0; i < count; i++) {
+ todel.push(this[index + i]);
+ }
+ for (; i < this.length - index; i++) {
+ tail.push(this[index + i]);
+ }
+ var arglen = args.length;
+ for (i = 0; i < arglen + tail.length; i++) {
+ this.items[index + i] = this[index + i] = i < arglen ? args[i] : tail[i - arglen];
+ }
+ i = this.items.length = this.length -= count - arglen;
+ while (this[i]) {
+ delete this[i++];
+ }
+ return new Set(todel);
+ };
+ /*\
+ * Set.exclude
+ [ method ]
+ **
+ * Removes given element from the set
+ **
+ > Parameters
+ **
+ - element (object) element to remove
+ = (boolean) `true` if object was found & removed from the set
+ \*/
+ setproto.exclude = function (el) {
+ for (var i = 0, ii = this.length; i < ii; i++) if (this[i] == el) {
+ this.splice(i, 1);
+ return true;
+ }
+ };
+ setproto.animate = function (params, ms, easing, callback) {
+ (R.is(easing, "function") || !easing) && (callback = easing || null);
+ var len = this.items.length,
+ i = len,
+ item,
+ set = this,
+ collector;
+ if (!len) {
+ return this;
+ }
+ callback && (collector = function () {
+ !--len && callback.call(set);
+ });
+ easing = R.is(easing, string) ? easing : collector;
+ var anim = R.animation(params, ms, easing, collector);
+ item = this.items[--i].animate(anim);
+ while (i--) {
+ this.items[i] && !this.items[i].removed && this.items[i].animateWith(item, anim, anim);
+ (this.items[i] && !this.items[i].removed) || len--;
+ }
+ return this;
+ };
+ setproto.insertAfter = function (el) {
+ var i = this.items.length;
+ while (i--) {
+ this.items[i].insertAfter(el);
+ }
+ return this;
+ };
+ setproto.getBBox = function () {
+ var x = [],
+ y = [],
+ x2 = [],
+ y2 = [];
+ for (var i = this.items.length; i--;) if (!this.items[i].removed) {
+ var box = this.items[i].getBBox();
+ x.push(box.x);
+ y.push(box.y);
+ x2.push(box.x + box.width);
+ y2.push(box.y + box.height);
+ }
+ x = mmin[apply](0, x);
+ y = mmin[apply](0, y);
+ x2 = mmax[apply](0, x2);
+ y2 = mmax[apply](0, y2);
+ return {
+ x: x,
+ y: y,
+ x2: x2,
+ y2: y2,
+ width: x2 - x,
+ height: y2 - y
+ };
+ };
+ setproto.clone = function (s) {
+ s = this.paper.set();
+ for (var i = 0, ii = this.items.length; i < ii; i++) {
+ s.push(this.items[i].clone());
+ }
+ return s;
+ };
+ setproto.toString = function () {
+ return "Rapha\xebl\u2018s set";
+ };
+
+ setproto.glow = function(glowConfig) {
+ var ret = this.paper.set();
+ this.forEach(function(shape, index){
+ var g = shape.glow(glowConfig);
+ if(g != null){
+ g.forEach(function(shape2, index2){
+ ret.push(shape2);
+ });
+ }
+ });
+ return ret;
+ };
+
+
+ /*\
+ * Set.isPointInside
+ [ method ]
+ **
+ * Determine if given point is inside this set’s elements
+ **
+ > Parameters
+ **
+ - x (number) x coordinate of the point
+ - y (number) y coordinate of the point
+ = (boolean) `true` if point is inside any of the set's elements
+ \*/
+ setproto.isPointInside = function (x, y) {
+ var isPointInside = false;
+ this.forEach(function (el) {
+ if (el.isPointInside(x, y)) {
+ isPointInside = true;
+ return false; // stop loop
+ }
+ });
+ return isPointInside;
+ };
+
+ /*\
+ * Raphael.registerFont
+ [ method ]
+ **
+ * Adds given font to the registered set of fonts for Raphaël. Should be used as an internal call from within Cufón’s font file.
+ * Returns original parameter, so it could be used with chaining.
+ # <a href="http://wiki.github.com/sorccu/cufon/about">More about Cufón and how to convert your font form TTF, OTF, etc to JavaScript file.</a>
+ **
+ > Parameters
+ **
+ - font (object) the font to register
+ = (object) the font you passed in
+ > Usage
+ | Cufon.registerFont(Raphael.registerFont({…}));
+ \*/
+ R.registerFont = function (font) {
+ if (!font.face) {
+ return font;
+ }
+ this.fonts = this.fonts || {};
+ var fontcopy = {
+ w: font.w,
+ face: {},
+ glyphs: {}
+ },
+ family = font.face["font-family"];
+ for (var prop in font.face) if (font.face[has](prop)) {
+ fontcopy.face[prop] = font.face[prop];
+ }
+ if (this.fonts[family]) {
+ this.fonts[family].push(fontcopy);
+ } else {
+ this.fonts[family] = [fontcopy];
+ }
+ if (!font.svg) {
+ fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10);
+ for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) {
+ var path = font.glyphs[glyph];
+ fontcopy.glyphs[glyph] = {
+ w: path.w,
+ k: {},
+ d: path.d && "M" + path.d.replace(/[mlcxtrv]/g, function (command) {
+ return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M";
+ }) + "z"
+ };
+ if (path.k) {
+ for (var k in path.k) if (path[has](k)) {
+ fontcopy.glyphs[glyph].k[k] = path.k[k];
+ }
+ }
+ }
+ }
+ return font;
+ };
+ /*\
+ * Paper.getFont
+ [ method ]
+ **
+ * Finds font object in the registered fonts by given parameters. You could specify only one word from the font name, like “Myriad” for “Myriad Pro”.
+ **
+ > Parameters
+ **
+ - family (string) font family name or any word from it
+ - weight (string) #optional font weight
+ - style (string) #optional font style
+ - stretch (string) #optional font stretch
+ = (object) the font object
+ > Usage
+ | paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30);
+ \*/
+ paperproto.getFont = function (family, weight, style, stretch) {
+ stretch = stretch || "normal";
+ style = style || "normal";
+ weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400;
+ if (!R.fonts) {
+ return;
+ }
+ var font = R.fonts[family];
+ if (!font) {
+ var name = new RegExp("(^|\\s)" + family.replace(/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i");
+ for (var fontName in R.fonts) if (R.fonts[has](fontName)) {
+ if (name.test(fontName)) {
+ font = R.fonts[fontName];
+ break;
+ }
+ }
+ }
+ var thefont;
+ if (font) {
+ for (var i = 0, ii = font.length; i < ii; i++) {
+ thefont = font[i];
+ if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) {
+ break;
+ }
+ }
+ }
+ return thefont;
+ };
+ /*\
+ * Paper.print
+ [ method ]
+ **
+ * Creates path that represent given text written using given font at given position with given size.
+ * Result of the method is path element that contains whole text as a separate path.
+ **
+ > Parameters
+ **
+ - x (number) x position of the text
+ - y (number) y position of the text
+ - string (string) text to print
+ - font (object) font object, see @Paper.getFont
+ - size (number) #optional size of the font, default is `16`
+ - origin (string) #optional could be `"baseline"` or `"middle"`, default is `"middle"`
+ - letter_spacing (number) #optional number in range `-1..1`, default is `0`
+ - line_spacing (number) #optional number in range `1..3`, default is `1`
+ = (object) resulting path element, which consist of all letters
+ > Usage
+ | var txt = r.print(10, 50, "print", r.getFont("Museo"), 30).attr({fill: "#fff"});
+ \*/
+ paperproto.print = function (x, y, string, font, size, origin, letter_spacing, line_spacing) {
+ origin = origin || "middle"; // baseline|middle
+ letter_spacing = mmax(mmin(letter_spacing || 0, 1), -1);
+ line_spacing = mmax(mmin(line_spacing || 1, 3), 1);
+ var letters = Str(string)[split](E),
+ shift = 0,
+ notfirst = 0,
+ path = E,
+ scale;
+ R.is(font, "string") && (font = this.getFont(font));
+ if (font) {
+ scale = (size || 16) / font.face["units-per-em"];
+ var bb = font.face.bbox[split](separator),
+ top = +bb[0],
+ lineHeight = bb[3] - bb[1],
+ shifty = 0,
+ height = +bb[1] + (origin == "baseline" ? lineHeight + (+font.face.descent) : lineHeight / 2);
+ for (var i = 0, ii = letters.length; i < ii; i++) {
+ if (letters[i] == "\n") {
+ shift = 0;
+ curr = 0;
+ notfirst = 0;
+ shifty += lineHeight * line_spacing;
+ } else {
+ var prev = notfirst && font.glyphs[letters[i - 1]] || {},
+ curr = font.glyphs[letters[i]];
+ shift += notfirst ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) + (font.w * letter_spacing) : 0;
+ notfirst = 1;
+ }
+ if (curr && curr.d) {
+ path += R.transformPath(curr.d, ["t", shift * scale, shifty * scale, "s", scale, scale, top, height, "t", (x - top) / scale, (y - height) / scale]);
+ }
+ }
+ }
+ return this.path(path).attr({
+ fill: "#000",
+ stroke: "none"
+ });
+ };
+
+ /*\
+ * Paper.add
+ [ method ]
+ **
+ * Imports elements in JSON array in format `{type: type, <attributes>}`
+ **
+ > Parameters
+ **
+ - json (array)
+ = (object) resulting set of imported elements
+ > Usage
+ | paper.add([
+ | {
+ | type: "circle",
+ | cx: 10,
+ | cy: 10,
+ | r: 5
+ | },
+ | {
+ | type: "rect",
+ | x: 10,
+ | y: 10,
+ | width: 10,
+ | height: 10,
+ | fill: "#fc0"
+ | }
+ | ]);
+ \*/
+ paperproto.add = function (json) {
+ if (R.is(json, "array")) {
+ var res = this.set(),
+ i = 0,
+ ii = json.length,
+ j;
+ for (; i < ii; i++) {
+ j = json[i] || {};
+ elements[has](j.type) && res.push(this[j.type]().attr(j));
+ }
+ }
+ return res;
+ };
+
+ /*\
+ * Raphael.format
+ [ method ]
+ **
+ * Simple format function. Replaces construction of type “`{<number>}`” to the corresponding argument.
+ **
+ > Parameters
+ **
+ - token (string) string to format
+ - … (string) rest of arguments will be treated as parameters for replacement
+ = (string) formated string
+ > Usage
+ | var x = 10,
+ | y = 20,
+ | width = 40,
+ | height = 50;
+ | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
+ | paper.path(Raphael.format("M{0},{1}h{2}v{3}h{4}z", x, y, width, height, -width));
+ \*/
+ R.format = function (token, params) {
+ var args = R.is(params, array) ? [0][concat](params) : arguments;
+ token && R.is(token, string) && args.length - 1 && (token = token.replace(formatrg, function (str, i) {
+ return args[++i] == null ? E : args[i];
+ }));
+ return token || E;
+ };
+ /*\
+ * Raphael.fullfill
+ [ method ]
+ **
+ * A little bit more advanced format function than @Raphael.format. Replaces construction of type “`{<name>}`” to the corresponding argument.
+ **
+ > Parameters
+ **
+ - token (string) string to format
+ - json (object) object which properties will be used as a replacement
+ = (string) formated string
+ > Usage
+ | // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z"
+ | paper.path(Raphael.fullfill("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", {
+ | x: 10,
+ | y: 20,
+ | dim: {
+ | width: 40,
+ | height: 50,
+ | "negative width": -40
+ | }
+ | }));
+ \*/
+ R.fullfill = (function () {
+ var tokenRegex = /\{([^\}]+)\}/g,
+ objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties
+ replacer = function (all, key, obj) {
+ var res = obj;
+ key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) {
+ name = name || quotedName;
+ if (res) {
+ if (name in res) {
+ res = res[name];
+ }
+ typeof res == "function" && isFunc && (res = res());
+ }
+ });
+ res = (res == null || res == obj ? all : res) + "";
+ return res;
+ };
+ return function (str, obj) {
+ return String(str).replace(tokenRegex, function (all, key) {
+ return replacer(all, key, obj);
+ });
+ };
+ })();
+ /*\
+ * Raphael.ninja
+ [ method ]
+ **
+ * If you want to leave no trace of Raphaël (Well, Raphaël creates only one global variable `Raphael`, but anyway.) You can use `ninja` method.
+ * Beware, that in this case plugins could stop working, because they are depending on global variable existance.
+ **
+ = (object) Raphael object
+ > Usage
+ | (function (local_raphael) {
+ | var paper = local_raphael(10, 10, 320, 200);
+ | …
+ | })(Raphael.ninja());
+ \*/
+ R.ninja = function () {
+ oldRaphael.was ? (g.win.Raphael = oldRaphael.is) : delete Raphael;
+ return R;
+ };
+ /*\
+ * Raphael.st
+ [ property (object) ]
+ **
+ * You can add your own method to elements and sets. It is wise to add a set method for each element method
+ * you added, so you will be able to call the same method on sets too.
+ **
+ * See also @Raphael.el.
+ > Usage
+ | Raphael.el.red = function () {
+ | this.attr({fill: "#f00"});
+ | };
+ | Raphael.st.red = function () {
+ | this.forEach(function (el) {
+ | el.red();
+ | });
+ | };
+ | // then use it
+ | paper.set(paper.circle(100, 100, 20), paper.circle(110, 100, 20)).red();
+ \*/
+ R.st = setproto;
+
+ eve.on("raphael.DOMload", function () {
+ loaded = true;
+ });
+
+ // Firefox <3.6 fix: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
+ (function (doc, loaded, f) {
+ if (doc.readyState == null && doc.addEventListener){
+ doc.addEventListener(loaded, f = function () {
+ doc.removeEventListener(loaded, f, false);
+ doc.readyState = "complete";
+ }, false);
+ doc.readyState = "loading";
+ }
+ function isLoaded() {
+ (/in/).test(doc.readyState) ? setTimeout(isLoaded, 9) : R.eve("raphael.DOMload");
+ }
+ isLoaded();
+ })(document, "DOMContentLoaded");
+
+// ┌─────────────────────────────────────────────────────────────────────┐ \\
+// │ Raphaël - JavaScript Vector Library │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ SVG Module │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
+// └─────────────────────────────────────────────────────────────────────┘ \\
+
+(function(){
+ if (!R.svg) {
+ return;
+ }
+ var has = "hasOwnProperty",
+ Str = String,
+ toFloat = parseFloat,
+ toInt = parseInt,
+ math = Math,
+ mmax = math.max,
+ abs = math.abs,
+ pow = math.pow,
+ separator = /[, ]+/,
+ eve = R.eve,
+ E = "",
+ S = " ";
+ var xlink = "http://www.w3.org/1999/xlink",
+ markers = {
+ block: "M5,0 0,2.5 5,5z",
+ classic: "M5,0 0,2.5 5,5 3.5,3 3.5,2z",
+ diamond: "M2.5,0 5,2.5 2.5,5 0,2.5z",
+ open: "M6,1 1,3.5 6,6",
+ oval: "M2.5,0A2.5,2.5,0,0,1,2.5,5 2.5,2.5,0,0,1,2.5,0z"
+ },
+ markerCounter = {};
+ R.toString = function () {
+ return "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version;
+ };
+ var $ = function (el, attr) {
+ if (attr) {
+ if (typeof el == "string") {
+ el = $(el);
+ }
+ for (var key in attr) if (attr[has](key)) {
+ if (key.substring(0, 6) == "xlink:") {
+ el.setAttributeNS(xlink, key.substring(6), Str(attr[key]));
+ } else {
+ el.setAttribute(key, Str(attr[key]));
+ }
+ }
+ } else {
+ el = R._g.doc.createElementNS("http://www.w3.org/2000/svg", el);
+ el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)");
+ }
+ return el;
+ },
+ addGradientFill = function (element, gradient) {
+ var type = "linear",
+ id = element.id + gradient,
+ fx = .5, fy = .5,
+ o = element.node,
+ SVG = element.paper,
+ s = o.style,
+ el = R._g.doc.getElementById(id);
+ if (!el) {
+ gradient = Str(gradient).replace(R._radial_gradient, function (all, _fx, _fy) {
+ type = "radial";
+ if (_fx && _fy) {
+ fx = toFloat(_fx);
+ fy = toFloat(_fy);
+ var dir = ((fy > .5) * 2 - 1);
+ pow(fx - .5, 2) + pow(fy - .5, 2) > .25 &&
+ (fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) &&
+ fy != .5 &&
+ (fy = fy.toFixed(5) - 1e-5 * dir);
+ }
+ return E;
+ });
+ gradient = gradient.split(/\s*\-\s*/);
+ if (type == "linear") {
+ var angle = gradient.shift();
+ angle = -toFloat(angle);
+ if (isNaN(angle)) {
+ return null;
+ }
+ var vector = [0, 0, math.cos(R.rad(angle)), math.sin(R.rad(angle))],
+ max = 1 / (mmax(abs(vector[2]), abs(vector[3])) || 1);
+ vector[2] *= max;
+ vector[3] *= max;
+ if (vector[2] < 0) {
+ vector[0] = -vector[2];
+ vector[2] = 0;
+ }
+ if (vector[3] < 0) {
+ vector[1] = -vector[3];
+ vector[3] = 0;
+ }
+ }
+ var dots = R._parseDots(gradient);
+ if (!dots) {
+ return null;
+ }
+ id = id.replace(/[\(\)\s,\xb0#]/g, "_");
+
+ if (element.gradient && id != element.gradient.id) {
+ SVG.defs.removeChild(element.gradient);
+ delete element.gradient;
+ }
+
+ if (!element.gradient) {
+ el = $(type + "Gradient", {id: id});
+ element.gradient = el;
+ $(el, type == "radial" ? {
+ fx: fx,
+ fy: fy
+ } : {
+ x1: vector[0],
+ y1: vector[1],
+ x2: vector[2],
+ y2: vector[3],
+ gradientTransform: element.matrix.invert()
+ });
+ SVG.defs.appendChild(el);
+ for (var i = 0, ii = dots.length; i < ii; i++) {
+ el.appendChild($("stop", {
+ offset: dots[i].offset ? dots[i].offset : i ? "100%" : "0%",
+ "stop-color": dots[i].color || "#fff"
+ }));
+ }
+ }
+ }
+ $(o, {
+ fill: "url('" + document.location + "#" + id + "')",
+ opacity: 1,
+ "fill-opacity": 1
+ });
+ s.fill = E;
+ s.opacity = 1;
+ s.fillOpacity = 1;
+ return 1;
+ },
+ updatePosition = function (o) {
+ var bbox = o.getBBox(1);
+ $(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"});
+ },
+ addArrow = function (o, value, isEnd) {
+ if (o.type == "path") {
+ var values = Str(value).toLowerCase().split("-"),
+ p = o.paper,
+ se = isEnd ? "end" : "start",
+ node = o.node,
+ attrs = o.attrs,
+ stroke = attrs["stroke-width"],
+ i = values.length,
+ type = "classic",
+ from,
+ to,
+ dx,
+ refX,
+ attr,
+ w = 3,
+ h = 3,
+ t = 5;
+ while (i--) {
+ switch (values[i]) {
+ case "block":
+ case "classic":
+ case "oval":
+ case "diamond":
+ case "open":
+ case "none":
+ type = values[i];
+ break;
+ case "wide": h = 5; break;
+ case "narrow": h = 2; break;
+ case "long": w = 5; break;
+ case "short": w = 2; break;
+ }
+ }
+ if (type == "open") {
+ w += 2;
+ h += 2;
+ t += 2;
+ dx = 1;
+ refX = isEnd ? 4 : 1;
+ attr = {
+ fill: "none",
+ stroke: attrs.stroke
+ };
+ } else {
+ refX = dx = w / 2;
+ attr = {
+ fill: attrs.stroke,
+ stroke: "none"
+ };
+ }
+ if (o._.arrows) {
+ if (isEnd) {
+ o._.arrows.endPath && markerCounter[o._.arrows.endPath]--;
+ o._.arrows.endMarker && markerCounter[o._.arrows.endMarker]--;
+ } else {
+ o._.arrows.startPath && markerCounter[o._.arrows.startPath]--;
+ o._.arrows.startMarker && markerCounter[o._.arrows.startMarker]--;
+ }
+ } else {
+ o._.arrows = {};
+ }
+ if (type != "none") {
+ var pathId = "raphael-marker-" + type,
+ markerId = "raphael-marker-" + se + type + w + h + "-obj" + o.id;
+ if (!R._g.doc.getElementById(pathId)) {
+ p.defs.appendChild($($("path"), {
+ "stroke-linecap": "round",
+ d: markers[type],
+ id: pathId
+ }));
+ markerCounter[pathId] = 1;
+ } else {
+ markerCounter[pathId]++;
+ }
+ var marker = R._g.doc.getElementById(markerId),
+ use;
+ if (!marker) {
+ marker = $($("marker"), {
+ id: markerId,
+ markerHeight: h,
+ markerWidth: w,
+ orient: "auto",
+ refX: refX,
+ refY: h / 2
+ });
+ use = $($("use"), {
+ "xlink:href": "#" + pathId,
+ transform: (isEnd ? "rotate(180 " + w / 2 + " " + h / 2 + ") " : E) + "scale(" + w / t + "," + h / t + ")",
+ "stroke-width": (1 / ((w / t + h / t) / 2)).toFixed(4)
+ });
+ marker.appendChild(use);
+ p.defs.appendChild(marker);
+ markerCounter[markerId] = 1;
+ } else {
+ markerCounter[markerId]++;
+ use = marker.getElementsByTagName("use")[0];
+ }
+ $(use, attr);
+ var delta = dx * (type != "diamond" && type != "oval");
+ if (isEnd) {
+ from = o._.arrows.startdx * stroke || 0;
+ to = R.getTotalLength(attrs.path) - delta * stroke;
+ } else {
+ from = delta * stroke;
+ to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
+ }
+ attr = {};
+ attr["marker-" + se] = "url(#" + markerId + ")";
+ if (to || from) {
+ attr.d = R.getSubpath(attrs.path, from, to);
+ }
+ $(node, attr);
+ o._.arrows[se + "Path"] = pathId;
+ o._.arrows[se + "Marker"] = markerId;
+ o._.arrows[se + "dx"] = delta;
+ o._.arrows[se + "Type"] = type;
+ o._.arrows[se + "String"] = value;
+ } else {
+ if (isEnd) {
+ from = o._.arrows.startdx * stroke || 0;
+ to = R.getTotalLength(attrs.path) - from;
+ } else {
+ from = 0;
+ to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0);
+ }
+ o._.arrows[se + "Path"] && $(node, {d: R.getSubpath(attrs.path, from, to)});
+ delete o._.arrows[se + "Path"];
+ delete o._.arrows[se + "Marker"];
+ delete o._.arrows[se + "dx"];
+ delete o._.arrows[se + "Type"];
+ delete o._.arrows[se + "String"];
+ }
+ for (attr in markerCounter) if (markerCounter[has](attr) && !markerCounter[attr]) {
+ var item = R._g.doc.getElementById(attr);
+ item && item.parentNode.removeChild(item);
+ }
+ }
+ },
+ dasharray = {
+ "": [0],
+ "none": [0],
+ "-": [3, 1],
+ ".": [1, 1],
+ "-.": [3, 1, 1, 1],
+ "-..": [3, 1, 1, 1, 1, 1],
+ ". ": [1, 3],
+ "- ": [4, 3],
+ "--": [8, 3],
+ "- .": [4, 3, 1, 3],
+ "--.": [8, 3, 1, 3],
+ "--..": [8, 3, 1, 3, 1, 3]
+ },
+ addDashes = function (o, value, params) {
+ value = dasharray[Str(value).toLowerCase()];
+ if (value) {
+ var width = o.attrs["stroke-width"] || "1",
+ butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0,
+ dashes = [],
+ i = value.length;
+ while (i--) {
+ dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt;
+ }
+ $(o.node, {"stroke-dasharray": dashes.join(",")});
+ }
+ },
+ setFillAndStroke = function (o, params) {
+ var node = o.node,
+ attrs = o.attrs,
+ vis = node.style.visibility;
+ node.style.visibility = "hidden";
+ for (var att in params) {
+ if (params[has](att)) {
+ if (!R._availableAttrs[has](att)) {
+ continue;
+ }
+ var value = params[att];
+ attrs[att] = value;
+ switch (att) {
+ case "blur":
+ o.blur(value);
+ break;
+ case "title":
+ var title = node.getElementsByTagName("title");
+
+ // Use the existing <title>.
+ if (title.length && (title = title[0])) {
+ title.firstChild.nodeValue = value;
+ } else {
+ title = $("title");
+ var val = R._g.doc.createTextNode(value);
+ title.appendChild(val);
+ node.appendChild(title);
+ }
+ break;
+ case "href":
+ case "target":
+ var pn = node.parentNode;
+ if (pn.tagName.toLowerCase() != "a") {
+ var hl = $("a");
+ pn.insertBefore(hl, node);
+ hl.appendChild(node);
+ pn = hl;
+ }
+ if (att == "target") {
+ pn.setAttributeNS(xlink, "show", value == "blank" ? "new" : value);
+ } else {
+ pn.setAttributeNS(xlink, att, value);
+ }
+ break;
+ case "cursor":
+ node.style.cursor = value;
+ break;
+ case "transform":
+ o.transform(value);
+ break;
+ case "arrow-start":
+ addArrow(o, value);
+ break;
+ case "arrow-end":
+ addArrow(o, value, 1);
+ break;
+ case "clip-rect":
+ var rect = Str(value).split(separator);
+ if (rect.length == 4) {
+ o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode);
+ var el = $("clipPath"),
+ rc = $("rect");
+ el.id = R.createUUID();
+ $(rc, {
+ x: rect[0],
+ y: rect[1],
+ width: rect[2],
+ height: rect[3]
+ });
+ el.appendChild(rc);
+ o.paper.defs.appendChild(el);
+ $(node, {"clip-path": "url(#" + el.id + ")"});
+ o.clip = rc;
+ }
+ if (!value) {
+ var path = node.getAttribute("clip-path");
+ if (path) {
+ var clip = R._g.doc.getElementById(path.replace(/(^url\(#|\)$)/g, E));
+ clip && clip.parentNode.removeChild(clip);
+ $(node, {"clip-path": E});
+ delete o.clip;
+ }
+ }
+ break;
+ case "path":
+ if (o.type == "path") {
+ $(node, {d: value ? attrs.path = R._pathToAbsolute(value) : "M0,0"});
+ o._.dirty = 1;
+ if (o._.arrows) {
+ "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
+ "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
+ }
+ }
+ break;
+ case "width":
+ node.setAttribute(att, value);
+ o._.dirty = 1;
+ if (attrs.fx) {
+ att = "x";
+ value = attrs.x;
+ } else {
+ break;
+ }
+ case "x":
+ if (attrs.fx) {
+ value = -attrs.x - (attrs.width || 0);
+ }
+ case "rx":
+ if (att == "rx" && o.type == "rect") {
+ break;
+ }
+ case "cx":
+ node.setAttribute(att, value);
+ o.pattern && updatePosition(o);
+ o._.dirty = 1;
+ break;
+ case "height":
+ node.setAttribute(att, value);
+ o._.dirty = 1;
+ if (attrs.fy) {
+ att = "y";
+ value = attrs.y;
+ } else {
+ break;
+ }
+ case "y":
+ if (attrs.fy) {
+ value = -attrs.y - (attrs.height || 0);
+ }
+ case "ry":
+ if (att == "ry" && o.type == "rect") {
+ break;
+ }
+ case "cy":
+ node.setAttribute(att, value);
+ o.pattern && updatePosition(o);
+ o._.dirty = 1;
+ break;
+ case "r":
+ if (o.type == "rect") {
+ $(node, {rx: value, ry: value});
+ } else {
+ node.setAttribute(att, value);
+ }
+ o._.dirty = 1;
+ break;
+ case "src":
+ if (o.type == "image") {
+ node.setAttributeNS(xlink, "href", value);
+ }
+ break;
+ case "stroke-width":
+ if (o._.sx != 1 || o._.sy != 1) {
+ value /= mmax(abs(o._.sx), abs(o._.sy)) || 1;
+ }
+ node.setAttribute(att, value);
+ if (attrs["stroke-dasharray"]) {
+ addDashes(o, attrs["stroke-dasharray"], params);
+ }
+ if (o._.arrows) {
+ "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
+ "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
+ }
+ break;
+ case "stroke-dasharray":
+ addDashes(o, value, params);
+ break;
+ case "fill":
+ var isURL = Str(value).match(R._ISURL);
+ if (isURL) {
+ el = $("pattern");
+ var ig = $("image");
+ el.id = R.createUUID();
+ $(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1});
+ $(ig, {x: 0, y: 0, "xlink:href": isURL[1]});
+ el.appendChild(ig);
+
+ (function (el) {
+ R._preload(isURL[1], function () {
+ var w = this.offsetWidth,
+ h = this.offsetHeight;
+ $(el, {width: w, height: h});
+ $(ig, {width: w, height: h});
+ o.paper.safari();
+ });
+ })(el);
+ o.paper.defs.appendChild(el);
+ $(node, {fill: "url(#" + el.id + ")"});
+ o.pattern = el;
+ o.pattern && updatePosition(o);
+ break;
+ }
+ var clr = R.getRGB(value);
+ if (!clr.error) {
+ delete params.gradient;
+ delete attrs.gradient;
+ !R.is(attrs.opacity, "undefined") &&
+ R.is(params.opacity, "undefined") &&
+ $(node, {opacity: attrs.opacity});
+ !R.is(attrs["fill-opacity"], "undefined") &&
+ R.is(params["fill-opacity"], "undefined") &&
+ $(node, {"fill-opacity": attrs["fill-opacity"]});
+ } else if ((o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value)) {
+ if ("opacity" in attrs || "fill-opacity" in attrs) {
+ var gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
+ if (gradient) {
+ var stops = gradient.getElementsByTagName("stop");
+ $(stops[stops.length - 1], {"stop-opacity": ("opacity" in attrs ? attrs.opacity : 1) * ("fill-opacity" in attrs ? attrs["fill-opacity"] : 1)});
+ }
+ }
+ attrs.gradient = value;
+ attrs.fill = "none";
+ break;
+ }
+ clr[has]("opacity") && $(node, {"fill-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
+ case "stroke":
+ clr = R.getRGB(value);
+ node.setAttribute(att, clr.hex);
+ att == "stroke" && clr[has]("opacity") && $(node, {"stroke-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity});
+ if (att == "stroke" && o._.arrows) {
+ "startString" in o._.arrows && addArrow(o, o._.arrows.startString);
+ "endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1);
+ }
+ break;
+ case "gradient":
+ (o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value);
+ break;
+ case "opacity":
+ if (attrs.gradient && !attrs[has]("stroke-opacity")) {
+ $(node, {"stroke-opacity": value > 1 ? value / 100 : value});
+ }
+ // fall
+ case "fill-opacity":
+ if (attrs.gradient) {
+ gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E));
+ if (gradient) {
+ stops = gradient.getElementsByTagName("stop");
+ $(stops[stops.length - 1], {"stop-opacity": value});
+ }
+ break;
+ }
+ default:
+ att == "font-size" && (value = toInt(value, 10) + "px");
+ var cssrule = att.replace(/(\-.)/g, function (w) {
+ return w.substring(1).toUpperCase();
+ });
+ node.style[cssrule] = value;
+ o._.dirty = 1;
+ node.setAttribute(att, value);
+ break;
+ }
+ }
+ }
+
+ tuneText(o, params);
+ node.style.visibility = vis;
+ },
+ leading = 1.2,
+ tuneText = function (el, params) {
+ if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) {
+ return;
+ }
+ var a = el.attrs,
+ node = el.node,
+ fontSize = node.firstChild ? toInt(R._g.doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10;
+
+ if (params[has]("text")) {
+ a.text = params.text;
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+ var texts = Str(params.text).split("\n"),
+ tspans = [],
+ tspan;
+ for (var i = 0, ii = texts.length; i < ii; i++) {
+ tspan = $("tspan");
+ i && $(tspan, {dy: fontSize * leading, x: a.x});
+ tspan.appendChild(R._g.doc.createTextNode(texts[i]));
+ node.appendChild(tspan);
+ tspans[i] = tspan;
+ }
+ } else {
+ tspans = node.getElementsByTagName("tspan");
+ for (i = 0, ii = tspans.length; i < ii; i++) if (i) {
+ $(tspans[i], {dy: fontSize * leading, x: a.x});
+ } else {
+ $(tspans[0], {dy: 0});
+ }
+ }
+ $(node, {x: a.x, y: a.y});
+ el._.dirty = 1;
+ var bb = el._getBBox(),
+ dif = a.y - (bb.y + bb.height / 2);
+ dif && R.is(dif, "finite") && $(tspans[0], {dy: dif});
+ },
+ getRealNode = function (node) {
+ if (node.parentNode && node.parentNode.tagName.toLowerCase() === "a") {
+ return node.parentNode;
+ } else {
+ return node;
+ }
+ },
+ Element = function (node, svg) {
+ var X = 0,
+ Y = 0;
+ /*\
+ * Element.node
+ [ property (object) ]
+ **
+ * Gives you a reference to the DOM object, so you can assign event handlers or just mess around.
+ **
+ * Note: Don’t mess with it.
+ > Usage
+ | // draw a circle at coordinate 10,10 with radius of 10
+ | var c = paper.circle(10, 10, 10);
+ | c.node.onclick = function () {
+ | c.attr("fill", "red");
+ | };
+ \*/
+ this[0] = this.node = node;
+ /*\
+ * Element.raphael
+ [ property (object) ]
+ **
+ * Internal reference to @Raphael object. In case it is not available.
+ > Usage
+ | Raphael.el.red = function () {
+ | var hsb = this.paper.raphael.rgb2hsb(this.attr("fill"));
+ | hsb.h = 1;
+ | this.attr({fill: this.paper.raphael.hsb2rgb(hsb).hex});
+ | }
+ \*/
+ node.raphael = true;
+ /*\
+ * Element.id
+ [ property (number) ]
+ **
+ * Unique id of the element. Especially useful when you want to listen to events of the element,
+ * because all events are fired in format `<module>.<action>.<id>`. Also useful for @Paper.getById method.
+ \*/
+ this.id = R._oid++;
+ node.raphaelid = this.id;
+ this.matrix = R.matrix();
+ this.realPath = null;
+ /*\
+ * Element.paper
+ [ property (object) ]
+ **
+ * Internal reference to “paper” where object drawn. Mainly for use in plugins and element extensions.
+ > Usage
+ | Raphael.el.cross = function () {
+ | this.attr({fill: "red"});
+ | this.paper.path("M10,10L50,50M50,10L10,50")
+ | .attr({stroke: "red"});
+ | }
+ \*/
+ this.paper = svg;
+ this.attrs = this.attrs || {};
+ this._ = {
+ transform: [],
+ sx: 1,
+ sy: 1,
+ deg: 0,
+ dx: 0,
+ dy: 0,
+ dirty: 1
+ };
+ !svg.bottom && (svg.bottom = this);
+ /*\
+ * Element.prev
+ [ property (object) ]
+ **
+ * Reference to the previous element in the hierarchy.
+ \*/
+ this.prev = svg.top;
+ svg.top && (svg.top.next = this);
+ svg.top = this;
+ /*\
+ * Element.next
+ [ property (object) ]
+ **
+ * Reference to the next element in the hierarchy.
+ \*/
+ this.next = null;
+ },
+ elproto = R.el;
+
+ Element.prototype = elproto;
+ elproto.constructor = Element;
+
+ R._engine.path = function (pathString, SVG) {
+ var el = $("path");
+ SVG.canvas && SVG.canvas.appendChild(el);
+ var p = new Element(el, SVG);
+ p.type = "path";
+ setFillAndStroke(p, {
+ fill: "none",
+ stroke: "#000",
+ path: pathString
+ });
+ return p;
+ };
+ /*\
+ * Element.rotate
+ [ method ]
+ **
+ * Deprecated! Use @Element.transform instead.
+ * Adds rotation by given angle around given point to the list of
+ * transformations of the element.
+ > Parameters
+ - deg (number) angle in degrees
+ - cx (number) #optional x coordinate of the centre of rotation
+ - cy (number) #optional y coordinate of the centre of rotation
+ * If cx & cy aren’t specified centre of the shape is used as a point of rotation.
+ = (object) @Element
+ \*/
+ elproto.rotate = function (deg, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ deg = Str(deg).split(separator);
+ if (deg.length - 1) {
+ cx = toFloat(deg[1]);
+ cy = toFloat(deg[2]);
+ }
+ deg = toFloat(deg[0]);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ cx = bbox.x + bbox.width / 2;
+ cy = bbox.y + bbox.height / 2;
+ }
+ this.transform(this._.transform.concat([["r", deg, cx, cy]]));
+ return this;
+ };
+ /*\
+ * Element.scale
+ [ method ]
+ **
+ * Deprecated! Use @Element.transform instead.
+ * Adds scale by given amount relative to given point to the list of
+ * transformations of the element.
+ > Parameters
+ - sx (number) horisontal scale amount
+ - sy (number) vertical scale amount
+ - cx (number) #optional x coordinate of the centre of scale
+ - cy (number) #optional y coordinate of the centre of scale
+ * If cx & cy aren’t specified centre of the shape is used instead.
+ = (object) @Element
+ \*/
+ elproto.scale = function (sx, sy, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ sx = Str(sx).split(separator);
+ if (sx.length - 1) {
+ sy = toFloat(sx[1]);
+ cx = toFloat(sx[2]);
+ cy = toFloat(sx[3]);
+ }
+ sx = toFloat(sx[0]);
+ (sy == null) && (sy = sx);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ }
+ cx = cx == null ? bbox.x + bbox.width / 2 : cx;
+ cy = cy == null ? bbox.y + bbox.height / 2 : cy;
+ this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
+ return this;
+ };
+ /*\
+ * Element.translate
+ [ method ]
+ **
+ * Deprecated! Use @Element.transform instead.
+ * Adds translation by given amount to the list of transformations of the element.
+ > Parameters
+ - dx (number) horisontal shift
+ - dy (number) vertical shift
+ = (object) @Element
+ \*/
+ elproto.translate = function (dx, dy) {
+ if (this.removed) {
+ return this;
+ }
+ dx = Str(dx).split(separator);
+ if (dx.length - 1) {
+ dy = toFloat(dx[1]);
+ }
+ dx = toFloat(dx[0]) || 0;
+ dy = +dy || 0;
+ this.transform(this._.transform.concat([["t", dx, dy]]));
+ return this;
+ };
+ /*\
+ * Element.transform
+ [ method ]
+ **
+ * Adds transformation to the element which is separate to other attributes,
+ * i.e. translation doesn’t change `x` or `y` of the rectange. The format
+ * of transformation string is similar to the path string syntax:
+ | "t100,100r30,100,100s2,2,100,100r45s1.5"
+ * Each letter is a command. There are four commands: `t` is for translate, `r` is for rotate, `s` is for
+ * scale and `m` is for matrix.
+ *
+ * There are also alternative “absolute” translation, rotation and scale: `T`, `R` and `S`. They will not take previous transformation into account. For example, `...T100,0` will always move element 100 px horisontally, while `...t100,0` could move it vertically if there is `r90` before. Just compare results of `r90t100,0` and `r90T100,0`.
+ *
+ * So, the example line above could be read like “translate by 100, 100; rotate 30° around 100, 100; scale twice around 100, 100;
+ * rotate 45° around centre; scale 1.5 times relative to centre”. As you can see rotate and scale commands have origin
+ * coordinates as optional parameters, the default is the centre point of the element.
+ * Matrix accepts six parameters.
+ > Usage
+ | var el = paper.rect(10, 20, 300, 200);
+ | // translate 100, 100, rotate 45°, translate -100, 0
+ | el.transform("t100,100r45t-100,0");
+ | // if you want you can append or prepend transformations
+ | el.transform("...t50,50");
+ | el.transform("s2...");
+ | // or even wrap
+ | el.transform("t50,50...t-50-50");
+ | // to reset transformation call method with empty string
+ | el.transform("");
+ | // to get current value call it without parameters
+ | console.log(el.transform());
+ > Parameters
+ - tstr (string) #optional transformation string
+ * If tstr isn’t specified
+ = (string) current transformation string
+ * else
+ = (object) @Element
+ \*/
+ elproto.transform = function (tstr) {
+ var _ = this._;
+ if (tstr == null) {
+ return _.transform;
+ }
+ R._extractTransform(this, tstr);
+
+ this.clip && $(this.clip, {transform: this.matrix.invert()});
+ this.pattern && updatePosition(this);
+ this.node && $(this.node, {transform: this.matrix});
+
+ if (_.sx != 1 || _.sy != 1) {
+ var sw = this.attrs[has]("stroke-width") ? this.attrs["stroke-width"] : 1;
+ this.attr({"stroke-width": sw});
+ }
+
+ return this;
+ };
+ /*\
+ * Element.hide
+ [ method ]
+ **
+ * Makes element invisible. See @Element.show.
+ = (object) @Element
+ \*/
+ elproto.hide = function () {
+ !this.removed && this.paper.safari(this.node.style.display = "none");
+ return this;
+ };
+ /*\
+ * Element.show
+ [ method ]
+ **
+ * Makes element visible. See @Element.hide.
+ = (object) @Element
+ \*/
+ elproto.show = function () {
+ !this.removed && this.paper.safari(this.node.style.display = "");
+ return this;
+ };
+ /*\
+ * Element.remove
+ [ method ]
+ **
+ * Removes element from the paper.
+ \*/
+ elproto.remove = function () {
+ var node = getRealNode(this.node);
+ if (this.removed || !node.parentNode) {
+ return;
+ }
+ var paper = this.paper;
+ paper.__set__ && paper.__set__.exclude(this);
+ eve.unbind("raphael.*.*." + this.id);
+ if (this.gradient) {
+ paper.defs.removeChild(this.gradient);
+ }
+ R._tear(this, paper);
+
+ node.parentNode.removeChild(node);
+
+ // Remove custom data for element
+ this.removeData();
+
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ this.removed = true;
+ };
+ elproto._getBBox = function () {
+ if (this.node.style.display == "none") {
+ this.show();
+ var hide = true;
+ }
+ var canvasHidden = false,
+ containerStyle;
+ if (this.paper.canvas.parentElement) {
+ containerStyle = this.paper.canvas.parentElement.style;
+ } //IE10+ can't find parentElement
+ else if (this.paper.canvas.parentNode) {
+ containerStyle = this.paper.canvas.parentNode.style;
+ }
+
+ if(containerStyle && containerStyle.display == "none") {
+ canvasHidden = true;
+ containerStyle.display = "";
+ }
+ var bbox = {};
+ try {
+ bbox = this.node.getBBox();
+ } catch(e) {
+ // Firefox 3.0.x, 25.0.1 (probably more versions affected) play badly here - possible fix
+ bbox = {
+ x: this.node.clientLeft,
+ y: this.node.clientTop,
+ width: this.node.clientWidth,
+ height: this.node.clientHeight
+ }
+ } finally {
+ bbox = bbox || {};
+ if(canvasHidden){
+ containerStyle.display = "none";
+ }
+ }
+ hide && this.hide();
+ return bbox;
+ };
+ /*\
+ * Element.attr
+ [ method ]
+ **
+ * Sets the attributes of the element.
+ > Parameters
+ - attrName (string) attribute’s name
+ - value (string) value
+ * or
+ - params (object) object of name/value pairs
+ * or
+ - attrName (string) attribute’s name
+ * or
+ - attrNames (array) in this case method returns array of current values for given attribute names
+ = (object) @Element if attrsName & value or params are passed in.
+ = (...) value of the attribute if only attrsName is passed in.
+ = (array) array of values of the attribute if attrsNames is passed in.
+ = (object) object of attributes if nothing is passed in.
+ > Possible parameters
+ # <p>Please refer to the <a href="http://www.w3.org/TR/SVG/" title="The W3C Recommendation for the SVG language describes these properties in detail.">SVG specification</a> for an explanation of these parameters.</p>
+ o arrow-end (string) arrowhead on the end of the path. The format for string is `<type>[-<width>[-<length>]]`. Possible types: `classic`, `block`, `open`, `oval`, `diamond`, `none`, width: `wide`, `narrow`, `medium`, length: `long`, `short`, `midium`.
+ o clip-rect (string) comma or space separated values: x, y, width and height
+ o cursor (string) CSS type of the cursor
+ o cx (number) the x-axis coordinate of the center of the circle, or ellipse
+ o cy (number) the y-axis coordinate of the center of the circle, or ellipse
+ o fill (string) colour, gradient or image
+ o fill-opacity (number)
+ o font (string)
+ o font-family (string)
+ o font-size (number) font size in pixels
+ o font-weight (string)
+ o height (number)
+ o href (string) URL, if specified element behaves as hyperlink
+ o opacity (number)
+ o path (string) SVG path string format
+ o r (number) radius of the circle, ellipse or rounded corner on the rect
+ o rx (number) horisontal radius of the ellipse
+ o ry (number) vertical radius of the ellipse
+ o src (string) image URL, only works for @Element.image element
+ o stroke (string) stroke colour
+ o stroke-dasharray (string) [“”, “`-`”, “`.`”, “`-.`”, “`-..`”, “`. `”, “`- `”, “`--`”, “`- .`”, “`--.`”, “`--..`”]
+ o stroke-linecap (string) [“`butt`”, “`square`”, “`round`”]
+ o stroke-linejoin (string) [“`bevel`”, “`round`”, “`miter`”]
+ o stroke-miterlimit (number)
+ o stroke-opacity (number)
+ o stroke-width (number) stroke width in pixels, default is '1'
+ o target (string) used with href
+ o text (string) contents of the text element. Use `\n` for multiline text
+ o text-anchor (string) [“`start`”, “`middle`”, “`end`”], default is “`middle`”
+ o title (string) will create tooltip with a given text
+ o transform (string) see @Element.transform
+ o width (number)
+ o x (number)
+ o y (number)
+ > Gradients
+ * Linear gradient format: “`‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`90-#fff-#000`” – 90°
+ * gradient from white to black or “`0-#fff-#f00:20-#000`” – 0° gradient from white via red (at 20%) to black.
+ *
+ * radial gradient: “`r[(‹fx›, ‹fy›)]‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`r#fff-#000`” –
+ * gradient from white to black or “`r(0.25, 0.75)#fff-#000`” – gradient from white to black with focus point
+ * at 0.25, 0.75. Focus point coordinates are in 0..1 range. Radial gradients can only be applied to circles and ellipses.
+ > Path String
+ # <p>Please refer to <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path’s data attribute’s format are described in the SVG specification.">SVG documentation regarding path string</a>. Raphaël fully supports it.</p>
+ > Colour Parsing
+ # <ul>
+ # <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li>
+ # <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li>
+ # <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li>
+ # <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200,&nbsp;100,&nbsp;0)</code>”)</li>
+ # <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%,&nbsp;175%,&nbsp;0%)</code>”)</li>
+ # <li>rgba(•••, •••, •••, •••) — red, green and blue channels’ values: (“<code>rgba(200,&nbsp;100,&nbsp;0, .5)</code>”)</li>
+ # <li>rgba(•••%, •••%, •••%, •••%) — same as above, but in %: (“<code>rgba(100%,&nbsp;175%,&nbsp;0%, 50%)</code>”)</li>
+ # <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5,&nbsp;0.25,&nbsp;1)</code>”)</li>
+ # <li>hsb(•••%, •••%, •••%) — same as above, but in %</li>
+ # <li>hsba(•••, •••, •••, •••) — same as above, but with opacity</li>
+ # <li>hsl(•••, •••, •••) — almost the same as hsb, see <a href="http://en.wikipedia.org/wiki/HSL_and_HSV" title="HSL and HSV - Wikipedia, the free encyclopedia">Wikipedia page</a></li>
+ # <li>hsl(•••%, •••%, •••%) — same as above, but in %</li>
+ # <li>hsla(•••, •••, •••, •••) — same as above, but with opacity</li>
+ # <li>Optionally for hsb and hsl you could specify hue as a degree: “<code>hsl(240deg,&nbsp;1,&nbsp;.5)</code>” or, if you want to go fancy, “<code>hsl(240°,&nbsp;1,&nbsp;.5)</code>”</li>
+ # </ul>
+ \*/
+ elproto.attr = function (name, value) {
+ if (this.removed) {
+ return this;
+ }
+ if (name == null) {
+ var res = {};
+ for (var a in this.attrs) if (this.attrs[has](a)) {
+ res[a] = this.attrs[a];
+ }
+ res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
+ res.transform = this._.transform;
+ return res;
+ }
+ if (value == null && R.is(name, "string")) {
+ if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) {
+ return this.attrs.gradient;
+ }
+ if (name == "transform") {
+ return this._.transform;
+ }
+ var names = name.split(separator),
+ out = {};
+ for (var i = 0, ii = names.length; i < ii; i++) {
+ name = names[i];
+ if (name in this.attrs) {
+ out[name] = this.attrs[name];
+ } else if (R.is(this.paper.customAttributes[name], "function")) {
+ out[name] = this.paper.customAttributes[name].def;
+ } else {
+ out[name] = R._availableAttrs[name];
+ }
+ }
+ return ii - 1 ? out : out[names[0]];
+ }
+ if (value == null && R.is(name, "array")) {
+ out = {};
+ for (i = 0, ii = name.length; i < ii; i++) {
+ out[name[i]] = this.attr(name[i]);
+ }
+ return out;
+ }
+ if (value != null) {
+ var params = {};
+ params[name] = value;
+ } else if (name != null && R.is(name, "object")) {
+ params = name;
+ }
+ for (var key in params) {
+ eve("raphael.attr." + key + "." + this.id, this, params[key]);
+ }
+ for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
+ var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
+ this.attrs[key] = params[key];
+ for (var subkey in par) if (par[has](subkey)) {
+ params[subkey] = par[subkey];
+ }
+ }
+ setFillAndStroke(this, params);
+ return this;
+ };
+ /*\
+ * Element.toFront
+ [ method ]
+ **
+ * Moves the element so it is the closest to the viewer’s eyes, on top of other elements.
+ = (object) @Element
+ \*/
+ elproto.toFront = function () {
+ if (this.removed) {
+ return this;
+ }
+ var node = getRealNode(this.node);
+ node.parentNode.appendChild(node);
+ var svg = this.paper;
+ svg.top != this && R._tofront(this, svg);
+ return this;
+ };
+ /*\
+ * Element.toBack
+ [ method ]
+ **
+ * Moves the element so it is the furthest from the viewer’s eyes, behind other elements.
+ = (object) @Element
+ \*/
+ elproto.toBack = function () {
+ if (this.removed) {
+ return this;
+ }
+ var node = getRealNode(this.node);
+ var parentNode = node.parentNode;
+ parentNode.insertBefore(node, parentNode.firstChild);
+ R._toback(this, this.paper);
+ var svg = this.paper;
+ return this;
+ };
+ /*\
+ * Element.insertAfter
+ [ method ]
+ **
+ * Inserts current object after the given one.
+ = (object) @Element
+ \*/
+ elproto.insertAfter = function (element) {
+ if (this.removed || !element) {
+ return this;
+ }
+
+ var node = getRealNode(this.node);
+ var afterNode = getRealNode(element.node || element[element.length - 1].node);
+ if (afterNode.nextSibling) {
+ afterNode.parentNode.insertBefore(node, afterNode.nextSibling);
+ } else {
+ afterNode.parentNode.appendChild(node);
+ }
+ R._insertafter(this, element, this.paper);
+ return this;
+ };
+ /*\
+ * Element.insertBefore
+ [ method ]
+ **
+ * Inserts current object before the given one.
+ = (object) @Element
+ \*/
+ elproto.insertBefore = function (element) {
+ if (this.removed || !element) {
+ return this;
+ }
+
+ var node = getRealNode(this.node);
+ var beforeNode = getRealNode(element.node || element[0].node);
+ beforeNode.parentNode.insertBefore(node, beforeNode);
+ R._insertbefore(this, element, this.paper);
+ return this;
+ };
+ elproto.blur = function (size) {
+ // Experimental. No Safari support. Use it on your own risk.
+ var t = this;
+ if (+size !== 0) {
+ var fltr = $("filter"),
+ blur = $("feGaussianBlur");
+ t.attrs.blur = size;
+ fltr.id = R.createUUID();
+ $(blur, {stdDeviation: +size || 1.5});
+ fltr.appendChild(blur);
+ t.paper.defs.appendChild(fltr);
+ t._blur = fltr;
+ $(t.node, {filter: "url(#" + fltr.id + ")"});
+ } else {
+ if (t._blur) {
+ t._blur.parentNode.removeChild(t._blur);
+ delete t._blur;
+ delete t.attrs.blur;
+ }
+ t.node.removeAttribute("filter");
+ }
+ return t;
+ };
+ R._engine.circle = function (svg, x, y, r) {
+ var el = $("circle");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {cx: x, cy: y, r: r, fill: "none", stroke: "#000"};
+ res.type = "circle";
+ $(el, res.attrs);
+ return res;
+ };
+ R._engine.rect = function (svg, x, y, w, h, r) {
+ var el = $("rect");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {x: x, y: y, width: w, height: h, rx: r || 0, ry: r || 0, fill: "none", stroke: "#000"};
+ res.type = "rect";
+ $(el, res.attrs);
+ return res;
+ };
+ R._engine.ellipse = function (svg, x, y, rx, ry) {
+ var el = $("ellipse");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {cx: x, cy: y, rx: rx, ry: ry, fill: "none", stroke: "#000"};
+ res.type = "ellipse";
+ $(el, res.attrs);
+ return res;
+ };
+ R._engine.image = function (svg, src, x, y, w, h) {
+ var el = $("image");
+ $(el, {x: x, y: y, width: w, height: h, preserveAspectRatio: "none"});
+ el.setAttributeNS(xlink, "href", src);
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {x: x, y: y, width: w, height: h, src: src};
+ res.type = "image";
+ return res;
+ };
+ R._engine.text = function (svg, x, y, text) {
+ var el = $("text");
+ svg.canvas && svg.canvas.appendChild(el);
+ var res = new Element(el, svg);
+ res.attrs = {
+ x: x,
+ y: y,
+ "text-anchor": "middle",
+ text: text,
+ "font-family": R._availableAttrs["font-family"],
+ "font-size": R._availableAttrs["font-size"],
+ stroke: "none",
+ fill: "#000"
+ };
+ res.type = "text";
+ setFillAndStroke(res, res.attrs);
+ return res;
+ };
+ R._engine.setSize = function (width, height) {
+ this.width = width || this.width;
+ this.height = height || this.height;
+ this.canvas.setAttribute("width", this.width);
+ this.canvas.setAttribute("height", this.height);
+ if (this._viewBox) {
+ this.setViewBox.apply(this, this._viewBox);
+ }
+ return this;
+ };
+ R._engine.create = function () {
+ var con = R._getContainer.apply(0, arguments),
+ container = con && con.container,
+ x = con.x,
+ y = con.y,
+ width = con.width,
+ height = con.height;
+ if (!container) {
+ throw new Error("SVG container not found.");
+ }
+ var cnvs = $("svg"),
+ css = "overflow:hidden;",
+ isFloating;
+ x = x || 0;
+ y = y || 0;
+ width = width || 512;
+ height = height || 342;
+ $(cnvs, {
+ height: height,
+ version: 1.1,
+ width: width,
+ xmlns: "http://www.w3.org/2000/svg",
+ "xmlns:xlink": "http://www.w3.org/1999/xlink"
+ });
+ if (container == 1) {
+ cnvs.style.cssText = css + "position:absolute;left:" + x + "px;top:" + y + "px";
+ R._g.doc.body.appendChild(cnvs);
+ isFloating = 1;
+ } else {
+ cnvs.style.cssText = css + "position:relative";
+ if (container.firstChild) {
+ container.insertBefore(cnvs, container.firstChild);
+ } else {
+ container.appendChild(cnvs);
+ }
+ }
+ container = new R._Paper;
+ container.width = width;
+ container.height = height;
+ container.canvas = cnvs;
+ container.clear();
+ container._left = container._top = 0;
+ isFloating && (container.renderfix = function () {});
+ container.renderfix();
+ return container;
+ };
+ R._engine.setViewBox = function (x, y, w, h, fit) {
+ eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
+ var paperSize = this.getSize(),
+ size = mmax(w / paperSize.width, h / paperSize.height),
+ top = this.top,
+ aspectRatio = fit ? "xMidYMid meet" : "xMinYMin",
+ vb,
+ sw;
+ if (x == null) {
+ if (this._vbSize) {
+ size = 1;
+ }
+ delete this._vbSize;
+ vb = "0 0 " + this.width + S + this.height;
+ } else {
+ this._vbSize = size;
+ vb = x + S + y + S + w + S + h;
+ }
+ $(this.canvas, {
+ viewBox: vb,
+ preserveAspectRatio: aspectRatio
+ });
+ while (size && top) {
+ sw = "stroke-width" in top.attrs ? top.attrs["stroke-width"] : 1;
+ top.attr({"stroke-width": sw});
+ top._.dirty = 1;
+ top._.dirtyT = 1;
+ top = top.prev;
+ }
+ this._viewBox = [x, y, w, h, !!fit];
+ return this;
+ };
+ /*\
+ * Paper.renderfix
+ [ method ]
+ **
+ * Fixes the issue of Firefox and IE9 regarding subpixel rendering. If paper is dependant
+ * on other elements after reflow it could shift half pixel which cause for lines to lost their crispness.
+ * This method fixes the issue.
+ **
+ Special thanks to Mariusz Nowak (http://www.medikoo.com/) for this method.
+ \*/
+ R.prototype.renderfix = function () {
+ var cnvs = this.canvas,
+ s = cnvs.style,
+ pos;
+ try {
+ pos = cnvs.getScreenCTM() || cnvs.createSVGMatrix();
+ } catch (e) {
+ pos = cnvs.createSVGMatrix();
+ }
+ var left = -pos.e % 1,
+ top = -pos.f % 1;
+ if (left || top) {
+ if (left) {
+ this._left = (this._left + left) % 1;
+ s.left = this._left + "px";
+ }
+ if (top) {
+ this._top = (this._top + top) % 1;
+ s.top = this._top + "px";
+ }
+ }
+ };
+ /*\
+ * Paper.clear
+ [ method ]
+ **
+ * Clears the paper, i.e. removes all the elements.
+ \*/
+ R.prototype.clear = function () {
+ R.eve("raphael.clear", this);
+ var c = this.canvas;
+ while (c.firstChild) {
+ c.removeChild(c.firstChild);
+ }
+ this.bottom = this.top = null;
+ (this.desc = $("desc")).appendChild(R._g.doc.createTextNode("Created with Rapha\xebl " + R.version));
+ c.appendChild(this.desc);
+ c.appendChild(this.defs = $("defs"));
+ };
+ /*\
+ * Paper.remove
+ [ method ]
+ **
+ * Removes the paper from the DOM.
+ \*/
+ R.prototype.remove = function () {
+ eve("raphael.remove", this);
+ this.canvas.parentNode && this.canvas.parentNode.removeChild(this.canvas);
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ };
+ var setproto = R.st;
+ for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
+ setproto[method] = (function (methodname) {
+ return function () {
+ var arg = arguments;
+ return this.forEach(function (el) {
+ el[methodname].apply(el, arg);
+ });
+ };
+ })(method);
+ }
+})();
+
+// ┌─────────────────────────────────────────────────────────────────────┐ \\
+// │ Raphaël - JavaScript Vector Library │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ VML Module │ \\
+// ├─────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\
+// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\
+// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\
+// └─────────────────────────────────────────────────────────────────────┘ \\
+
+(function(){
+ if (!R.vml) {
+ return;
+ }
+ var has = "hasOwnProperty",
+ Str = String,
+ toFloat = parseFloat,
+ math = Math,
+ round = math.round,
+ mmax = math.max,
+ mmin = math.min,
+ abs = math.abs,
+ fillString = "fill",
+ separator = /[, ]+/,
+ eve = R.eve,
+ ms = " progid:DXImageTransform.Microsoft",
+ S = " ",
+ E = "",
+ map = {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
+ bites = /([clmz]),?([^clmz]*)/gi,
+ blurregexp = / progid:\S+Blur\([^\)]+\)/g,
+ val = /-?[^,\s-]+/g,
+ cssDot = "position:absolute;left:0;top:0;width:1px;height:1px;behavior:url(#default#VML)",
+ zoom = 21600,
+ pathTypes = {path: 1, rect: 1, image: 1},
+ ovalTypes = {circle: 1, ellipse: 1},
+ path2vml = function (path) {
+ var total = /[ahqstv]/ig,
+ command = R._pathToAbsolute;
+ Str(path).match(total) && (command = R._path2curve);
+ total = /[clmz]/g;
+ if (command == R._pathToAbsolute && !Str(path).match(total)) {
+ var res = Str(path).replace(bites, function (all, command, args) {
+ var vals = [],
+ isMove = command.toLowerCase() == "m",
+ res = map[command];
+ args.replace(val, function (value) {
+ if (isMove && vals.length == 2) {
+ res += vals + map[command == "m" ? "l" : "L"];
+ vals = [];
+ }
+ vals.push(round(value * zoom));
+ });
+ return res + vals;
+ });
+ return res;
+ }
+ var pa = command(path), p, r;
+ res = [];
+ for (var i = 0, ii = pa.length; i < ii; i++) {
+ p = pa[i];
+ r = pa[i][0].toLowerCase();
+ r == "z" && (r = "x");
+ for (var j = 1, jj = p.length; j < jj; j++) {
+ r += round(p[j] * zoom) + (j != jj - 1 ? "," : E);
+ }
+ res.push(r);
+ }
+ return res.join(S);
+ },
+ compensation = function (deg, dx, dy) {
+ var m = R.matrix();
+ m.rotate(-deg, .5, .5);
+ return {
+ dx: m.x(dx, dy),
+ dy: m.y(dx, dy)
+ };
+ },
+ setCoords = function (p, sx, sy, dx, dy, deg) {
+ var _ = p._,
+ m = p.matrix,
+ fillpos = _.fillpos,
+ o = p.node,
+ s = o.style,
+ y = 1,
+ flip = "",
+ dxdy,
+ kx = zoom / sx,
+ ky = zoom / sy;
+ s.visibility = "hidden";
+ if (!sx || !sy) {
+ return;
+ }
+ o.coordsize = abs(kx) + S + abs(ky);
+ s.rotation = deg * (sx * sy < 0 ? -1 : 1);
+ if (deg) {
+ var c = compensation(deg, dx, dy);
+ dx = c.dx;
+ dy = c.dy;
+ }
+ sx < 0 && (flip += "x");
+ sy < 0 && (flip += " y") && (y = -1);
+ s.flip = flip;
+ o.coordorigin = (dx * -kx) + S + (dy * -ky);
+ if (fillpos || _.fillsize) {
+ var fill = o.getElementsByTagName(fillString);
+ fill = fill && fill[0];
+ o.removeChild(fill);
+ if (fillpos) {
+ c = compensation(deg, m.x(fillpos[0], fillpos[1]), m.y(fillpos[0], fillpos[1]));
+ fill.position = c.dx * y + S + c.dy * y;
+ }
+ if (_.fillsize) {
+ fill.size = _.fillsize[0] * abs(sx) + S + _.fillsize[1] * abs(sy);
+ }
+ o.appendChild(fill);
+ }
+ s.visibility = "visible";
+ };
+ R.toString = function () {
+ return "Your browser doesn\u2019t support SVG. Falling down to VML.\nYou are running Rapha\xebl " + this.version;
+ };
+ var addArrow = function (o, value, isEnd) {
+ var values = Str(value).toLowerCase().split("-"),
+ se = isEnd ? "end" : "start",
+ i = values.length,
+ type = "classic",
+ w = "medium",
+ h = "medium";
+ while (i--) {
+ switch (values[i]) {
+ case "block":
+ case "classic":
+ case "oval":
+ case "diamond":
+ case "open":
+ case "none":
+ type = values[i];
+ break;
+ case "wide":
+ case "narrow": h = values[i]; break;
+ case "long":
+ case "short": w = values[i]; break;
+ }
+ }
+ var stroke = o.node.getElementsByTagName("stroke")[0];
+ stroke[se + "arrow"] = type;
+ stroke[se + "arrowlength"] = w;
+ stroke[se + "arrowwidth"] = h;
+ },
+ setFillAndStroke = function (o, params) {
+ // o.paper.canvas.style.display = "none";
+ o.attrs = o.attrs || {};
+ var node = o.node,
+ a = o.attrs,
+ s = node.style,
+ xy,
+ newpath = pathTypes[o.type] && (params.x != a.x || params.y != a.y || params.width != a.width || params.height != a.height || params.cx != a.cx || params.cy != a.cy || params.rx != a.rx || params.ry != a.ry || params.r != a.r),
+ isOval = ovalTypes[o.type] && (a.cx != params.cx || a.cy != params.cy || a.r != params.r || a.rx != params.rx || a.ry != params.ry),
+ res = o;
+
+
+ for (var par in params) if (params[has](par)) {
+ a[par] = params[par];
+ }
+ if (newpath) {
+ a.path = R._getPath[o.type](o);
+ o._.dirty = 1;
+ }
+ params.href && (node.href = params.href);
+ params.title && (node.title = params.title);
+ params.target && (node.target = params.target);
+ params.cursor && (s.cursor = params.cursor);
+ "blur" in params && o.blur(params.blur);
+ if (params.path && o.type == "path" || newpath) {
+ node.path = path2vml(~Str(a.path).toLowerCase().indexOf("r") ? R._pathToAbsolute(a.path) : a.path);
+ o._.dirty = 1;
+ if (o.type == "image") {
+ o._.fillpos = [a.x, a.y];
+ o._.fillsize = [a.width, a.height];
+ setCoords(o, 1, 1, 0, 0, 0);
+ }
+ }
+ "transform" in params && o.transform(params.transform);
+ if (isOval) {
+ var cx = +a.cx,
+ cy = +a.cy,
+ rx = +a.rx || +a.r || 0,
+ ry = +a.ry || +a.r || 0;
+ node.path = R.format("ar{0},{1},{2},{3},{4},{1},{4},{1}x", round((cx - rx) * zoom), round((cy - ry) * zoom), round((cx + rx) * zoom), round((cy + ry) * zoom), round(cx * zoom));
+ o._.dirty = 1;
+ }
+ if ("clip-rect" in params) {
+ var rect = Str(params["clip-rect"]).split(separator);
+ if (rect.length == 4) {
+ rect[2] = +rect[2] + (+rect[0]);
+ rect[3] = +rect[3] + (+rect[1]);
+ var div = node.clipRect || R._g.doc.createElement("div"),
+ dstyle = div.style;
+ dstyle.clip = R.format("rect({1}px {2}px {3}px {0}px)", rect);
+ if (!node.clipRect) {
+ dstyle.position = "absolute";
+ dstyle.top = 0;
+ dstyle.left = 0;
+ dstyle.width = o.paper.width + "px";
+ dstyle.height = o.paper.height + "px";
+ node.parentNode.insertBefore(div, node);
+ div.appendChild(node);
+ node.clipRect = div;
+ }
+ }
+ if (!params["clip-rect"]) {
+ node.clipRect && (node.clipRect.style.clip = "auto");
+ }
+ }
+ if (o.textpath) {
+ var textpathStyle = o.textpath.style;
+ params.font && (textpathStyle.font = params.font);
+ params["font-family"] && (textpathStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(/^['"]+|['"]+$/g, E) + '"');
+ params["font-size"] && (textpathStyle.fontSize = params["font-size"]);
+ params["font-weight"] && (textpathStyle.fontWeight = params["font-weight"]);
+ params["font-style"] && (textpathStyle.fontStyle = params["font-style"]);
+ }
+ if ("arrow-start" in params) {
+ addArrow(res, params["arrow-start"]);
+ }
+ if ("arrow-end" in params) {
+ addArrow(res, params["arrow-end"], 1);
+ }
+ if (params.opacity != null ||
+ params["stroke-width"] != null ||
+ params.fill != null ||
+ params.src != null ||
+ params.stroke != null ||
+ params["stroke-width"] != null ||
+ params["stroke-opacity"] != null ||
+ params["fill-opacity"] != null ||
+ params["stroke-dasharray"] != null ||
+ params["stroke-miterlimit"] != null ||
+ params["stroke-linejoin"] != null ||
+ params["stroke-linecap"] != null) {
+ var fill = node.getElementsByTagName(fillString),
+ newfill = false;
+ fill = fill && fill[0];
+ !fill && (newfill = fill = createNode(fillString));
+ if (o.type == "image" && params.src) {
+ fill.src = params.src;
+ }
+ params.fill && (fill.on = true);
+ if (fill.on == null || params.fill == "none" || params.fill === null) {
+ fill.on = false;
+ }
+ if (fill.on && params.fill) {
+ var isURL = Str(params.fill).match(R._ISURL);
+ if (isURL) {
+ fill.parentNode == node && node.removeChild(fill);
+ fill.rotate = true;
+ fill.src = isURL[1];
+ fill.type = "tile";
+ var bbox = o.getBBox(1);
+ fill.position = bbox.x + S + bbox.y;
+ o._.fillpos = [bbox.x, bbox.y];
+
+ R._preload(isURL[1], function () {
+ o._.fillsize = [this.offsetWidth, this.offsetHeight];
+ });
+ } else {
+ fill.color = R.getRGB(params.fill).hex;
+ fill.src = E;
+ fill.type = "solid";
+ if (R.getRGB(params.fill).error && (res.type in {circle: 1, ellipse: 1} || Str(params.fill).charAt() != "r") && addGradientFill(res, params.fill, fill)) {
+ a.fill = "none";
+ a.gradient = params.fill;
+ fill.rotate = false;
+ }
+ }
+ }
+ if ("fill-opacity" in params || "opacity" in params) {
+ var opacity = ((+a["fill-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+R.getRGB(params.fill).o + 1 || 2) - 1);
+ opacity = mmin(mmax(opacity, 0), 1);
+ fill.opacity = opacity;
+ if (fill.src) {
+ fill.color = "none";
+ }
+ }
+ node.appendChild(fill);
+ var stroke = (node.getElementsByTagName("stroke") && node.getElementsByTagName("stroke")[0]),
+ newstroke = false;
+ !stroke && (newstroke = stroke = createNode("stroke"));
+ if ((params.stroke && params.stroke != "none") ||
+ params["stroke-width"] ||
+ params["stroke-opacity"] != null ||
+ params["stroke-dasharray"] ||
+ params["stroke-miterlimit"] ||
+ params["stroke-linejoin"] ||
+ params["stroke-linecap"]) {
+ stroke.on = true;
+ }
+ (params.stroke == "none" || params.stroke === null || stroke.on == null || params.stroke == 0 || params["stroke-width"] == 0) && (stroke.on = false);
+ var strokeColor = R.getRGB(params.stroke);
+ stroke.on && params.stroke && (stroke.color = strokeColor.hex);
+ opacity = ((+a["stroke-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+strokeColor.o + 1 || 2) - 1);
+ var width = (toFloat(params["stroke-width"]) || 1) * .75;
+ opacity = mmin(mmax(opacity, 0), 1);
+ params["stroke-width"] == null && (width = a["stroke-width"]);
+ params["stroke-width"] && (stroke.weight = width);
+ width && width < 1 && (opacity *= width) && (stroke.weight = 1);
+ stroke.opacity = opacity;
+
+ params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter");
+ stroke.miterlimit = params["stroke-miterlimit"] || 8;
+ params["stroke-linecap"] && (stroke.endcap = params["stroke-linecap"] == "butt" ? "flat" : params["stroke-linecap"] == "square" ? "square" : "round");
+ if ("stroke-dasharray" in params) {
+ var dasharray = {
+ "-": "shortdash",
+ ".": "shortdot",
+ "-.": "shortdashdot",
+ "-..": "shortdashdotdot",
+ ". ": "dot",
+ "- ": "dash",
+ "--": "longdash",
+ "- .": "dashdot",
+ "--.": "longdashdot",
+ "--..": "longdashdotdot"
+ };
+ stroke.dashstyle = dasharray[has](params["stroke-dasharray"]) ? dasharray[params["stroke-dasharray"]] : E;
+ }
+ newstroke && node.appendChild(stroke);
+ }
+ if (res.type == "text") {
+ res.paper.canvas.style.display = E;
+ var span = res.paper.span,
+ m = 100,
+ fontSize = a.font && a.font.match(/\d+(?:\.\d*)?(?=px)/);
+ s = span.style;
+ a.font && (s.font = a.font);
+ a["font-family"] && (s.fontFamily = a["font-family"]);
+ a["font-weight"] && (s.fontWeight = a["font-weight"]);
+ a["font-style"] && (s.fontStyle = a["font-style"]);
+ fontSize = toFloat(a["font-size"] || fontSize && fontSize[0]) || 10;
+ s.fontSize = fontSize * m + "px";
+ res.textpath.string && (span.innerHTML = Str(res.textpath.string).replace(/</g, "&#60;").replace(/&/g, "&#38;").replace(/\n/g, "<br>"));
+ var brect = span.getBoundingClientRect();
+ res.W = a.w = (brect.right - brect.left) / m;
+ res.H = a.h = (brect.bottom - brect.top) / m;
+ // res.paper.canvas.style.display = "none";
+ res.X = a.x;
+ res.Y = a.y + res.H / 2;
+
+ ("x" in params || "y" in params) && (res.path.v = R.format("m{0},{1}l{2},{1}", round(a.x * zoom), round(a.y * zoom), round(a.x * zoom) + 1));
+ var dirtyattrs = ["x", "y", "text", "font", "font-family", "font-weight", "font-style", "font-size"];
+ for (var d = 0, dd = dirtyattrs.length; d < dd; d++) if (dirtyattrs[d] in params) {
+ res._.dirty = 1;
+ break;
+ }
+
+ // text-anchor emulation
+ switch (a["text-anchor"]) {
+ case "start":
+ res.textpath.style["v-text-align"] = "left";
+ res.bbx = res.W / 2;
+ break;
+ case "end":
+ res.textpath.style["v-text-align"] = "right";
+ res.bbx = -res.W / 2;
+ break;
+ default:
+ res.textpath.style["v-text-align"] = "center";
+ res.bbx = 0;
+ break;
+ }
+ res.textpath.style["v-text-kern"] = true;
+ }
+ // res.paper.canvas.style.display = E;
+ },
+ addGradientFill = function (o, gradient, fill) {
+ o.attrs = o.attrs || {};
+ var attrs = o.attrs,
+ pow = Math.pow,
+ opacity,
+ oindex,
+ type = "linear",
+ fxfy = ".5 .5";
+ o.attrs.gradient = gradient;
+ gradient = Str(gradient).replace(R._radial_gradient, function (all, fx, fy) {
+ type = "radial";
+ if (fx && fy) {
+ fx = toFloat(fx);
+ fy = toFloat(fy);
+ pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && (fy = math.sqrt(.25 - pow(fx - .5, 2)) * ((fy > .5) * 2 - 1) + .5);
+ fxfy = fx + S + fy;
+ }
+ return E;
+ });
+ gradient = gradient.split(/\s*\-\s*/);
+ if (type == "linear") {
+ var angle = gradient.shift();
+ angle = -toFloat(angle);
+ if (isNaN(angle)) {
+ return null;
+ }
+ }
+ var dots = R._parseDots(gradient);
+ if (!dots) {
+ return null;
+ }
+ o = o.shape || o.node;
+ if (dots.length) {
+ o.removeChild(fill);
+ fill.on = true;
+ fill.method = "none";
+ fill.color = dots[0].color;
+ fill.color2 = dots[dots.length - 1].color;
+ var clrs = [];
+ for (var i = 0, ii = dots.length; i < ii; i++) {
+ dots[i].offset && clrs.push(dots[i].offset + S + dots[i].color);
+ }
+ fill.colors = clrs.length ? clrs.join() : "0% " + fill.color;
+ if (type == "radial") {
+ fill.type = "gradientTitle";
+ fill.focus = "100%";
+ fill.focussize = "0 0";
+ fill.focusposition = fxfy;
+ fill.angle = 0;
+ } else {
+ // fill.rotate= true;
+ fill.type = "gradient";
+ fill.angle = (270 - angle) % 360;
+ }
+ o.appendChild(fill);
+ }
+ return 1;
+ },
+ Element = function (node, vml) {
+ this[0] = this.node = node;
+ node.raphael = true;
+ this.id = R._oid++;
+ node.raphaelid = this.id;
+ this.X = 0;
+ this.Y = 0;
+ this.attrs = {};
+ this.paper = vml;
+ this.matrix = R.matrix();
+ this._ = {
+ transform: [],
+ sx: 1,
+ sy: 1,
+ dx: 0,
+ dy: 0,
+ deg: 0,
+ dirty: 1,
+ dirtyT: 1
+ };
+ !vml.bottom && (vml.bottom = this);
+ this.prev = vml.top;
+ vml.top && (vml.top.next = this);
+ vml.top = this;
+ this.next = null;
+ };
+ var elproto = R.el;
+
+ Element.prototype = elproto;
+ elproto.constructor = Element;
+ elproto.transform = function (tstr) {
+ if (tstr == null) {
+ return this._.transform;
+ }
+ var vbs = this.paper._viewBoxShift,
+ vbt = vbs ? "s" + [vbs.scale, vbs.scale] + "-1-1t" + [vbs.dx, vbs.dy] : E,
+ oldt;
+ if (vbs) {
+ oldt = tstr = Str(tstr).replace(/\.{3}|\u2026/g, this._.transform || E);
+ }
+ R._extractTransform(this, vbt + tstr);
+ var matrix = this.matrix.clone(),
+ skew = this.skew,
+ o = this.node,
+ split,
+ isGrad = ~Str(this.attrs.fill).indexOf("-"),
+ isPatt = !Str(this.attrs.fill).indexOf("url(");
+ matrix.translate(1, 1);
+ if (isPatt || isGrad || this.type == "image") {
+ skew.matrix = "1 0 0 1";
+ skew.offset = "0 0";
+ split = matrix.split();
+ if ((isGrad && split.noRotation) || !split.isSimple) {
+ o.style.filter = matrix.toFilter();
+ var bb = this.getBBox(),
+ bbt = this.getBBox(1),
+ dx = bb.x - bbt.x,
+ dy = bb.y - bbt.y;
+ o.coordorigin = (dx * -zoom) + S + (dy * -zoom);
+ setCoords(this, 1, 1, dx, dy, 0);
+ } else {
+ o.style.filter = E;
+ setCoords(this, split.scalex, split.scaley, split.dx, split.dy, split.rotate);
+ }
+ } else {
+ o.style.filter = E;
+ skew.matrix = Str(matrix);
+ skew.offset = matrix.offset();
+ }
+ if (oldt !== null) { // empty string value is true as well
+ this._.transform = oldt;
+ R._extractTransform(this, oldt);
+ }
+ return this;
+ };
+ elproto.rotate = function (deg, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ if (deg == null) {
+ return;
+ }
+ deg = Str(deg).split(separator);
+ if (deg.length - 1) {
+ cx = toFloat(deg[1]);
+ cy = toFloat(deg[2]);
+ }
+ deg = toFloat(deg[0]);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ cx = bbox.x + bbox.width / 2;
+ cy = bbox.y + bbox.height / 2;
+ }
+ this._.dirtyT = 1;
+ this.transform(this._.transform.concat([["r", deg, cx, cy]]));
+ return this;
+ };
+ elproto.translate = function (dx, dy) {
+ if (this.removed) {
+ return this;
+ }
+ dx = Str(dx).split(separator);
+ if (dx.length - 1) {
+ dy = toFloat(dx[1]);
+ }
+ dx = toFloat(dx[0]) || 0;
+ dy = +dy || 0;
+ if (this._.bbox) {
+ this._.bbox.x += dx;
+ this._.bbox.y += dy;
+ }
+ this.transform(this._.transform.concat([["t", dx, dy]]));
+ return this;
+ };
+ elproto.scale = function (sx, sy, cx, cy) {
+ if (this.removed) {
+ return this;
+ }
+ sx = Str(sx).split(separator);
+ if (sx.length - 1) {
+ sy = toFloat(sx[1]);
+ cx = toFloat(sx[2]);
+ cy = toFloat(sx[3]);
+ isNaN(cx) && (cx = null);
+ isNaN(cy) && (cy = null);
+ }
+ sx = toFloat(sx[0]);
+ (sy == null) && (sy = sx);
+ (cy == null) && (cx = cy);
+ if (cx == null || cy == null) {
+ var bbox = this.getBBox(1);
+ }
+ cx = cx == null ? bbox.x + bbox.width / 2 : cx;
+ cy = cy == null ? bbox.y + bbox.height / 2 : cy;
+
+ this.transform(this._.transform.concat([["s", sx, sy, cx, cy]]));
+ this._.dirtyT = 1;
+ return this;
+ };
+ elproto.hide = function () {
+ !this.removed && (this.node.style.display = "none");
+ return this;
+ };
+ elproto.show = function () {
+ !this.removed && (this.node.style.display = E);
+ return this;
+ };
+ // Needed to fix the vml setViewBox issues
+ elproto.auxGetBBox = R.el.getBBox;
+ elproto.getBBox = function(){
+ var b = this.auxGetBBox();
+ if (this.paper && this.paper._viewBoxShift)
+ {
+ var c = {};
+ var z = 1/this.paper._viewBoxShift.scale;
+ c.x = b.x - this.paper._viewBoxShift.dx;
+ c.x *= z;
+ c.y = b.y - this.paper._viewBoxShift.dy;
+ c.y *= z;
+ c.width = b.width * z;
+ c.height = b.height * z;
+ c.x2 = c.x + c.width;
+ c.y2 = c.y + c.height;
+ return c;
+ }
+ return b;
+ };
+ elproto._getBBox = function () {
+ if (this.removed) {
+ return {};
+ }
+ return {
+ x: this.X + (this.bbx || 0) - this.W / 2,
+ y: this.Y - this.H,
+ width: this.W,
+ height: this.H
+ };
+ };
+ elproto.remove = function () {
+ if (this.removed || !this.node.parentNode) {
+ return;
+ }
+ this.paper.__set__ && this.paper.__set__.exclude(this);
+ R.eve.unbind("raphael.*.*." + this.id);
+ R._tear(this, this.paper);
+ this.node.parentNode.removeChild(this.node);
+ this.shape && this.shape.parentNode.removeChild(this.shape);
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ this.removed = true;
+ };
+ elproto.attr = function (name, value) {
+ if (this.removed) {
+ return this;
+ }
+ if (name == null) {
+ var res = {};
+ for (var a in this.attrs) if (this.attrs[has](a)) {
+ res[a] = this.attrs[a];
+ }
+ res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient;
+ res.transform = this._.transform;
+ return res;
+ }
+ if (value == null && R.is(name, "string")) {
+ if (name == fillString && this.attrs.fill == "none" && this.attrs.gradient) {
+ return this.attrs.gradient;
+ }
+ var names = name.split(separator),
+ out = {};
+ for (var i = 0, ii = names.length; i < ii; i++) {
+ name = names[i];
+ if (name in this.attrs) {
+ out[name] = this.attrs[name];
+ } else if (R.is(this.paper.customAttributes[name], "function")) {
+ out[name] = this.paper.customAttributes[name].def;
+ } else {
+ out[name] = R._availableAttrs[name];
+ }
+ }
+ return ii - 1 ? out : out[names[0]];
+ }
+ if (this.attrs && value == null && R.is(name, "array")) {
+ out = {};
+ for (i = 0, ii = name.length; i < ii; i++) {
+ out[name[i]] = this.attr(name[i]);
+ }
+ return out;
+ }
+ var params;
+ if (value != null) {
+ params = {};
+ params[name] = value;
+ }
+ value == null && R.is(name, "object") && (params = name);
+ for (var key in params) {
+ eve("raphael.attr." + key + "." + this.id, this, params[key]);
+ }
+ if (params) {
+ for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) {
+ var par = this.paper.customAttributes[key].apply(this, [].concat(params[key]));
+ this.attrs[key] = params[key];
+ for (var subkey in par) if (par[has](subkey)) {
+ params[subkey] = par[subkey];
+ }
+ }
+ // this.paper.canvas.style.display = "none";
+ if (params.text && this.type == "text") {
+ this.textpath.string = params.text;
+ }
+ setFillAndStroke(this, params);
+ // this.paper.canvas.style.display = E;
+ }
+ return this;
+ };
+ elproto.toFront = function () {
+ !this.removed && this.node.parentNode.appendChild(this.node);
+ this.paper && this.paper.top != this && R._tofront(this, this.paper);
+ return this;
+ };
+ elproto.toBack = function () {
+ if (this.removed) {
+ return this;
+ }
+ if (this.node.parentNode.firstChild != this.node) {
+ this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild);
+ R._toback(this, this.paper);
+ }
+ return this;
+ };
+ elproto.insertAfter = function (element) {
+ if (this.removed) {
+ return this;
+ }
+ if (element.constructor == R.st.constructor) {
+ element = element[element.length - 1];
+ }
+ if (element.node.nextSibling) {
+ element.node.parentNode.insertBefore(this.node, element.node.nextSibling);
+ } else {
+ element.node.parentNode.appendChild(this.node);
+ }
+ R._insertafter(this, element, this.paper);
+ return this;
+ };
+ elproto.insertBefore = function (element) {
+ if (this.removed) {
+ return this;
+ }
+ if (element.constructor == R.st.constructor) {
+ element = element[0];
+ }
+ element.node.parentNode.insertBefore(this.node, element.node);
+ R._insertbefore(this, element, this.paper);
+ return this;
+ };
+ elproto.blur = function (size) {
+ var s = this.node.runtimeStyle,
+ f = s.filter;
+ f = f.replace(blurregexp, E);
+ if (+size !== 0) {
+ this.attrs.blur = size;
+ s.filter = f + S + ms + ".Blur(pixelradius=" + (+size || 1.5) + ")";
+ s.margin = R.format("-{0}px 0 0 -{0}px", round(+size || 1.5));
+ } else {
+ s.filter = f;
+ s.margin = 0;
+ delete this.attrs.blur;
+ }
+ return this;
+ };
+
+ R._engine.path = function (pathString, vml) {
+ var el = createNode("shape");
+ el.style.cssText = cssDot;
+ el.coordsize = zoom + S + zoom;
+ el.coordorigin = vml.coordorigin;
+ var p = new Element(el, vml),
+ attr = {fill: "none", stroke: "#000"};
+ pathString && (attr.path = pathString);
+ p.type = "path";
+ p.path = [];
+ p.Path = E;
+ setFillAndStroke(p, attr);
+ vml.canvas.appendChild(el);
+ var skew = createNode("skew");
+ skew.on = true;
+ el.appendChild(skew);
+ p.skew = skew;
+ p.transform(E);
+ return p;
+ };
+ R._engine.rect = function (vml, x, y, w, h, r) {
+ var path = R._rectPath(x, y, w, h, r),
+ res = vml.path(path),
+ a = res.attrs;
+ res.X = a.x = x;
+ res.Y = a.y = y;
+ res.W = a.width = w;
+ res.H = a.height = h;
+ a.r = r;
+ a.path = path;
+ res.type = "rect";
+ return res;
+ };
+ R._engine.ellipse = function (vml, x, y, rx, ry) {
+ var res = vml.path(),
+ a = res.attrs;
+ res.X = x - rx;
+ res.Y = y - ry;
+ res.W = rx * 2;
+ res.H = ry * 2;
+ res.type = "ellipse";
+ setFillAndStroke(res, {
+ cx: x,
+ cy: y,
+ rx: rx,
+ ry: ry
+ });
+ return res;
+ };
+ R._engine.circle = function (vml, x, y, r) {
+ var res = vml.path(),
+ a = res.attrs;
+ res.X = x - r;
+ res.Y = y - r;
+ res.W = res.H = r * 2;
+ res.type = "circle";
+ setFillAndStroke(res, {
+ cx: x,
+ cy: y,
+ r: r
+ });
+ return res;
+ };
+ R._engine.image = function (vml, src, x, y, w, h) {
+ var path = R._rectPath(x, y, w, h),
+ res = vml.path(path).attr({stroke: "none"}),
+ a = res.attrs,
+ node = res.node,
+ fill = node.getElementsByTagName(fillString)[0];
+ a.src = src;
+ res.X = a.x = x;
+ res.Y = a.y = y;
+ res.W = a.width = w;
+ res.H = a.height = h;
+ a.path = path;
+ res.type = "image";
+ fill.parentNode == node && node.removeChild(fill);
+ fill.rotate = true;
+ fill.src = src;
+ fill.type = "tile";
+ res._.fillpos = [x, y];
+ res._.fillsize = [w, h];
+ node.appendChild(fill);
+ setCoords(res, 1, 1, 0, 0, 0);
+ return res;
+ };
+ R._engine.text = function (vml, x, y, text) {
+ var el = createNode("shape"),
+ path = createNode("path"),
+ o = createNode("textpath");
+ x = x || 0;
+ y = y || 0;
+ text = text || "";
+ path.v = R.format("m{0},{1}l{2},{1}", round(x * zoom), round(y * zoom), round(x * zoom) + 1);
+ path.textpathok = true;
+ o.string = Str(text);
+ o.on = true;
+ el.style.cssText = cssDot;
+ el.coordsize = zoom + S + zoom;
+ el.coordorigin = "0 0";
+ var p = new Element(el, vml),
+ attr = {
+ fill: "#000",
+ stroke: "none",
+ font: R._availableAttrs.font,
+ text: text
+ };
+ p.shape = el;
+ p.path = path;
+ p.textpath = o;
+ p.type = "text";
+ p.attrs.text = Str(text);
+ p.attrs.x = x;
+ p.attrs.y = y;
+ p.attrs.w = 1;
+ p.attrs.h = 1;
+ setFillAndStroke(p, attr);
+ el.appendChild(o);
+ el.appendChild(path);
+ vml.canvas.appendChild(el);
+ var skew = createNode("skew");
+ skew.on = true;
+ el.appendChild(skew);
+ p.skew = skew;
+ p.transform(E);
+ return p;
+ };
+ R._engine.setSize = function (width, height) {
+ var cs = this.canvas.style;
+ this.width = width;
+ this.height = height;
+ width == +width && (width += "px");
+ height == +height && (height += "px");
+ cs.width = width;
+ cs.height = height;
+ cs.clip = "rect(0 " + width + " " + height + " 0)";
+ if (this._viewBox) {
+ R._engine.setViewBox.apply(this, this._viewBox);
+ }
+ return this;
+ };
+ R._engine.setViewBox = function (x, y, w, h, fit) {
+ R.eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]);
+ var paperSize = this.getSize(),
+ width = paperSize.width,
+ height = paperSize.height,
+ H, W;
+ if (fit) {
+ H = height / h;
+ W = width / w;
+ if (w * H < width) {
+ x -= (width - w * H) / 2 / H;
+ }
+ if (h * W < height) {
+ y -= (height - h * W) / 2 / W;
+ }
+ }
+ this._viewBox = [x, y, w, h, !!fit];
+ this._viewBoxShift = {
+ dx: -x,
+ dy: -y,
+ scale: paperSize
+ };
+ this.forEach(function (el) {
+ el.transform("...");
+ });
+ return this;
+ };
+ var createNode;
+ R._engine.initWin = function (win) {
+ var doc = win.document;
+ if (doc.styleSheets.length < 31) {
+ doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)");
+ } else {
+ // no more room, add to the existing one
+ // http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx
+ doc.styleSheets[0].addRule(".rvml", "behavior:url(#default#VML)");
+ }
+ try {
+ !doc.namespaces.rvml && doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
+ createNode = function (tagName) {
+ return doc.createElement('<rvml:' + tagName + ' class="rvml">');
+ };
+ } catch (e) {
+ createNode = function (tagName) {
+ return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
+ };
+ }
+ };
+ R._engine.initWin(R._g.win);
+ R._engine.create = function () {
+ var con = R._getContainer.apply(0, arguments),
+ container = con.container,
+ height = con.height,
+ s,
+ width = con.width,
+ x = con.x,
+ y = con.y;
+ if (!container) {
+ throw new Error("VML container not found.");
+ }
+ var res = new R._Paper,
+ c = res.canvas = R._g.doc.createElement("div"),
+ cs = c.style;
+ x = x || 0;
+ y = y || 0;
+ width = width || 512;
+ height = height || 342;
+ res.width = width;
+ res.height = height;
+ width == +width && (width += "px");
+ height == +height && (height += "px");
+ res.coordsize = zoom * 1e3 + S + zoom * 1e3;
+ res.coordorigin = "0 0";
+ res.span = R._g.doc.createElement("span");
+ res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;";
+ c.appendChild(res.span);
+ cs.cssText = R.format("top:0;left:0;width:{0};height:{1};display:inline-block;position:relative;clip:rect(0 {0} {1} 0);overflow:hidden", width, height);
+ if (container == 1) {
+ R._g.doc.body.appendChild(c);
+ cs.left = x + "px";
+ cs.top = y + "px";
+ cs.position = "absolute";
+ } else {
+ if (container.firstChild) {
+ container.insertBefore(c, container.firstChild);
+ } else {
+ container.appendChild(c);
+ }
+ }
+ res.renderfix = function () {};
+ return res;
+ };
+ R.prototype.clear = function () {
+ R.eve("raphael.clear", this);
+ this.canvas.innerHTML = E;
+ this.span = R._g.doc.createElement("span");
+ this.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;";
+ this.canvas.appendChild(this.span);
+ this.bottom = this.top = null;
+ };
+ R.prototype.remove = function () {
+ R.eve("raphael.remove", this);
+ this.canvas.parentNode.removeChild(this.canvas);
+ for (var i in this) {
+ this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null;
+ }
+ return true;
+ };
+
+ var setproto = R.st;
+ for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) {
+ setproto[method] = (function (methodname) {
+ return function () {
+ var arg = arguments;
+ return this.forEach(function (el) {
+ el[methodname].apply(el, arg);
+ });
+ };
+ })(method);
+ }
+})();
+
+ // EXPOSE
+ // SVG and VML are appended just before the EXPOSE line
+ // Even with AMD, Raphael should be defined globally
+ oldRaphael.was ? (g.win.Raphael = R) : (Raphael = R);
+
+ if(typeof exports == "object"){
+ module.exports = R;
+ }
+ return R;
+}));