summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelipe Artur <felipefac@gmail.com>2016-06-22 10:47:48 -0300
committerFelipe Artur <felipefac@gmail.com>2016-06-22 10:47:48 -0300
commit2674b548601b279ada46d4b218a9def6fd5b9f6d (patch)
treeca1f09225e4d5b80c800af521735bf34f04e16d0
parent8447c6b180297840d835a609d95808834f498d87 (diff)
parent6f6c6f68ea7cb976b6c1598e705ba8b2bdaf05a1 (diff)
downloadgitlab-ce-2674b548601b279ada46d4b218a9def6fd5b9f6d.tar.gz
merge master into issue_3359_3
-rw-r--r--.gitlab-ci.yml85
-rw-r--r--CHANGELOG130
-rw-r--r--Gemfile12
-rw-r--r--Gemfile.lock26
-rw-r--r--app/assets/javascripts/LabelManager.js.coffee12
-rw-r--r--app/assets/javascripts/api.js.coffee7
-rw-r--r--app/assets/javascripts/application.js.coffee80
-rw-r--r--app/assets/javascripts/awards_handler.coffee2
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.coffee23
-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.coffee6
-rw-r--r--app/assets/javascripts/blob/template_selector.js.coffee56
-rw-r--r--app/assets/javascripts/ci/build.coffee9
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee19
-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/gl_dropdown.js.coffee15
-rw-r--r--app/assets/javascripts/gl_form.js.coffee3
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee6
-rw-r--r--app/assets/javascripts/issuable.js.coffee57
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee4
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js.coffee3
-rw-r--r--app/assets/javascripts/labels_select.js.coffee29
-rw-r--r--app/assets/javascripts/layout_nav.js.coffee39
-rw-r--r--app/assets/javascripts/lib/common_utils.js.coffee41
-rw-r--r--app/assets/javascripts/lib/text_utility.js.coffee79
-rw-r--r--app/assets/javascripts/logo.js.coffee6
-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/merged_buttons.js.coffee30
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee10
-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/notes.js.coffee11
-rw-r--r--app/assets/javascripts/notifications_dropdown.js.coffee18
-rw-r--r--app/assets/javascripts/notifications_form.js.coffee3
-rw-r--r--app/assets/javascripts/project.js.coffee38
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee55
-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/star.js.coffee2
-rw-r--r--app/assets/javascripts/users/calendar.js.coffee17
-rw-r--r--app/assets/javascripts/users_select.js.coffee6
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/blank.scss23
-rw-r--r--app/assets/stylesheets/framework/blocks.scss20
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss10
-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/markdown_area.scss30
-rw-r--r--app/assets/stylesheets/framework/mixins.scss14
-rw-r--r--app/assets/stylesheets/framework/mobile.scss13
-rw-r--r--app/assets/stylesheets/framework/nav.scss46
-rw-r--r--app/assets/stylesheets/framework/panels.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss5
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss204
-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/commit.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss142
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/editor.scss12
-rw-r--r--app/assets/stylesheets/pages/environments.scss5
-rw-r--r--app/assets/stylesheets/pages/events.scss7
-rw-r--r--app/assets/stylesheets/pages/groups.scss17
-rw-r--r--app/assets/stylesheets/pages/help.scss7
-rw-r--r--app/assets/stylesheets/pages/issuable.scss31
-rw-r--r--app/assets/stylesheets/pages/labels.scss18
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss33
-rw-r--r--app/assets/stylesheets/pages/note_form.scss17
-rw-r--r--app/assets/stylesheets/pages/notes.scss35
-rw-r--r--app/assets/stylesheets/pages/profile.scss19
-rw-r--r--app/assets/stylesheets/pages/projects.scss67
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss22
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/assets/stylesheets/pages/tree.scss4
-rw-r--r--app/controllers/admin/appearances_controller.rb1
-rw-r--r--app/controllers/admin/runner_projects_controller.rb11
-rw-r--r--app/controllers/application_controller.rb27
-rw-r--r--app/controllers/autocomplete_controller.rb1
-rw-r--r--app/controllers/concerns/membership_actions.rb43
-rw-r--r--app/controllers/dashboard/todos_controller.rb25
-rw-r--r--app/controllers/groups/group_members_controller.rb29
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb48
-rw-r--r--app/controllers/notification_settings_controller.rb2
-rw-r--r--app/controllers/profiles/accounts_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb42
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb17
-rw-r--r--app/controllers/projects/builds_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb49
-rw-r--r--app/controllers/projects/merge_requests_controller.rb17
-rw-r--r--app/controllers/projects/pipelines_controller.rb4
-rw-r--r--app/controllers/projects/project_members_controller.rb36
-rw-r--r--app/controllers/projects/runner_projects_controller.rb4
-rw-r--r--app/controllers/projects/runners_controller.rb7
-rw-r--r--app/controllers/projects/tags_controller.rb6
-rw-r--r--app/controllers/projects/todos_controller.rb31
-rw-r--r--app/controllers/projects/wikis_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb73
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/snippets_finder.rb2
-rw-r--r--app/finders/todos_finder.rb4
-rw-r--r--app/helpers/application_helper.rb18
-rw-r--r--app/helpers/blob_helper.rb24
-rw-r--r--app/helpers/branches_helper.rb2
-rw-r--r--app/helpers/button_helper.rb20
-rw-r--r--app/helpers/ci_status_helper.rb8
-rw-r--r--app/helpers/commits_helper.rb30
-rw-r--r--app/helpers/diff_helper.rb5
-rw-r--r--app/helpers/gitlab_markdown_helper.rb15
-rw-r--r--app/helpers/gitlab_routing_helper.rb81
-rw-r--r--app/helpers/groups_helper.rb20
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/members_helper.rb45
-rw-r--r--app/helpers/nav_helper.rb20
-rw-r--r--app/helpers/notifications_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb26
-rw-r--r--app/helpers/time_helper.rb1
-rw-r--r--app/helpers/todos_helper.rb5
-rw-r--r--app/mailers/emails/groups.rb52
-rw-r--r--app/mailers/emails/members.rb86
-rw-r--r--app/mailers/emails/projects.rb63
-rw-r--r--app/mailers/notify.rb4
-rw-r--r--app/models/ability.rb22
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/blob.rb2
-rw-r--r--app/models/ci/build.rb60
-rw-r--r--app/models/ci/pipeline.rb60
-rw-r--r--app/models/ci/runner.rb23
-rw-r--r--app/models/commit.rb26
-rw-r--r--app/models/commit_status.rb5
-rw-r--r--app/models/concerns/access_requestable.rb16
-rw-r--r--app/models/concerns/awardable.rb8
-rw-r--r--app/models/concerns/importable.rb6
-rw-r--r--app/models/concerns/participable.rb10
-rw-r--r--app/models/concerns/referable.rb4
-rw-r--r--app/models/deployment.rb29
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/group.rb17
-rw-r--r--app/models/issue.rb16
-rw-r--r--app/models/jira_issue.rb2
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/member.rb51
-rw-r--r--app/models/members/group_member.rb8
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb11
-rw-r--r--app/models/merge_request_diff.rb3
-rw-r--r--app/models/note.rb28
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/personal_access_token.rb20
-rw-r--r--app/models/project.rb78
-rw-r--r--app/models/project_services/bamboo_service.rb44
-rw-r--r--app/models/project_services/issue_tracker_service.rb18
-rw-r--r--app/models/project_services/teamcity_service.rb37
-rw-r--r--app/models/project_team.rb42
-rw-r--r--app/models/repository.rb34
-rw-r--r--app/models/service.rb2
-rw-r--r--app/models/todo.rb1
-rw-r--r--app/models/user.rb53
-rw-r--r--app/services/ci/create_builds_service.rb53
-rw-r--r--app/services/ci/create_pipeline_service.rb24
-rw-r--r--app/services/ci/register_build_service.rb25
-rw-r--r--app/services/create_commit_builds_service.rb55
-rw-r--r--app/services/create_deployment_service.rb18
-rw-r--r--app/services/git_hooks_service.rb2
-rw-r--r--app/services/members/destroy_service.rb21
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/notification_service.rb43
-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.rb27
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/appearances/preview.html.haml34
-rw-r--r--app/views/admin/appearances/show.html.haml2
-rw-r--r--app/views/admin/application_settings/show.html.haml1
-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.haml26
-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/groups/show.html.haml33
-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/projects/show.html.haml49
-rw-r--r--app/views/admin/runners/index.html.haml124
-rw-r--r--app/views/admin/runners/show.html.haml4
-rw-r--r--app/views/admin/users/groups.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml199
-rw-r--r--app/views/admin/users/projects.html.haml3
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-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/_group_member.html.haml57
-rw-r--r--app/views/groups/group_members/index.html.haml15
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/groups/show.html.haml5
-rw-r--r--app/views/help/_shortcuts.html.haml6
-rw-r--r--app/views/import/github/status.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml25
-rw-r--r--app/views/issues/_issue.atom.builder20
-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.haml9
-rw-r--r--app/views/layouts/nav/_admin.html.haml138
-rw-r--r--app/views/layouts/nav/_admin_settings.html.haml31
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml30
-rw-r--r--app/views/layouts/nav/_group.html.haml6
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml36
-rw-r--r--app/views/layouts/nav/_profile.html.haml10
-rw-r--r--app/views/layouts/nav/_project.html.haml43
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml72
-rw-r--r--app/views/notify/group_access_granted_email.html.haml4
-rw-r--r--app/views/notify/group_access_granted_email.text.erb4
-rw-r--r--app/views/notify/group_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/group_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/group_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/group_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/group_member_invited_email.html.haml14
-rw-r--r--app/views/notify/group_member_invited_email.text.erb4
-rw-r--r--app/views/notify/member_access_denied_email.html.haml4
-rw-r--r--app/views/notify/member_access_denied_email.text.erb3
-rw-r--r--app/views/notify/member_access_granted_email.html.haml3
-rw-r--r--app/views/notify/member_access_granted_email.text.erb3
-rw-r--r--app/views/notify/member_access_requested_email.html.haml3
-rw-r--r--app/views/notify/member_access_requested_email.text.erb3
-rw-r--r--app/views/notify/member_invite_accepted_email.html.haml5
-rw-r--r--app/views/notify/member_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/member_invite_declined_email.html.haml4
-rw-r--r--app/views/notify/member_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/member_invited_email.html.haml13
-rw-r--r--app/views/notify/member_invited_email.text.erb4
-rw-r--r--app/views/notify/project_access_granted_email.html.haml5
-rw-r--r--app/views/notify/project_access_granted_email.text.erb4
-rw-r--r--app/views/notify/project_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/project_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/project_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/project_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/project_member_invited_email.html.haml13
-rw-r--r--app/views/notify/project_member_invited_email.text.erb4
-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.haml6
-rw-r--r--app/views/profiles/accounts/show.html.haml10
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml7
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml105
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml7
-rw-r--r--app/views/projects/_last_push.html.haml22
-rw-r--r--app/views/projects/_md_preview.html.haml15
-rw-r--r--app/views/projects/badges/index.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml12
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/builds/_sidebar.html.haml30
-rw-r--r--app/views/projects/builds/show.html.haml10
-rw-r--r--app/views/projects/buttons/_star.html.haml2
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml6
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml43
-rw-r--r--app/views/projects/commits/_commits.html.haml17
-rw-r--r--app/views/projects/commits/_head.html.haml10
-rw-r--r--app/views/projects/commits/show.html.haml11
-rw-r--r--app/views/projects/compare/index.html.haml2
-rw-r--r--app/views/projects/compare/show.html.haml36
-rw-r--r--app/views/projects/container_registry/_tag.html.haml22
-rw-r--r--app/views/projects/deployments/_commit.html.haml12
-rw-r--r--app/views/projects/deployments/_deployment.html.haml23
-rw-r--r--app/views/projects/diffs/_diffs.html.haml3
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/edit.html.haml36
-rw-r--r--app/views/projects/environments/_environment.html.haml17
-rw-r--r--app/views/projects/environments/_form.html.haml7
-rw-r--r--app/views/projects/environments/_header_title.html.haml1
-rw-r--r--app/views/projects/environments/index.html.haml31
-rw-r--r--app/views/projects/environments/new.html.haml12
-rw-r--r--app/views/projects/environments/show.html.haml37
-rw-r--r--app/views/projects/forks/index.html.haml6
-rw-r--r--app/views/projects/graphs/_head.html.haml26
-rw-r--r--app/views/projects/graphs/ci.html.haml25
-rw-r--r--app/views/projects/graphs/commits.html.haml90
-rw-r--r--app/views/projects/graphs/languages.html.haml36
-rw-r--r--app/views/projects/graphs/show.html.haml48
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_head.html.haml4
-rw-r--r--app/views/projects/labels/index.html.haml8
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-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/merge_requests/widget/_merged.html.haml59
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml4
-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/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml2
-rw-r--r--app/views/projects/notes/_hints.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/pipelines/_head.html.haml10
-rw-r--r--app/views/projects/project_members/_group_members.html.haml16
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/_project_member.html.haml55
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml9
-rw-r--r--app/views/projects/project_members/_team.html.haml6
-rw-r--r--app/views/projects/project_members/index.html.haml6
-rw-r--r--app/views/projects/project_members/update.js.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml6
-rw-r--r--app/views/projects/runners/_runner.html.haml6
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml10
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/show.html.haml8
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-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/projects/wikis/_main_links.html.haml3
-rw-r--r--app/views/projects/wikis/_nav.html.haml11
-rw-r--r--app/views/projects/wikis/_new.html.haml31
-rw-r--r--app/views/projects/wikis/edit.html.haml31
-rw-r--r--app/views/projects/wikis/git_access.html.haml52
-rw-r--r--app/views/projects/wikis/history.html.haml66
-rw-r--r--app/views/projects/wikis/pages.html.haml18
-rw-r--r--app/views/projects/wikis/show.html.haml34
-rw-r--r--app/views/shared/_event_filter.html.haml6
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/_ref_switcher.html.haml9
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml41
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml14
-rw-r--r--app/views/shared/members/_member.html.haml77
-rw-r--r--app/views/shared/members/_requests.html.haml8
-rw-r--r--app/views/shared/milestones/_merge_requests_tab.haml8
-rw-r--r--app/views/shared/milestones/_summary.html.haml7
-rw-r--r--app/views/shared/notifications/_button.html.haml (renamed from app/views/notifications/buttons/_notifications.html.haml)22
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml17
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml13
-rw-r--r--app/views/shared/projects/_list.html.haml6
-rw-r--r--app/views/u2f/_register.html.haml17
-rw-r--r--app/workers/expire_build_artifacts_worker.rb13
-rw-r--r--app/workers/gitlab_remove_project_export_worker.rb9
-rw-r--r--app/workers/project_export_worker.rb12
-rw-r--r--app/workers/repository_check/single_repository_worker.rb6
-rw-r--r--app/workers/stuck_ci_builds_worker.rb2
-rwxr-xr-xbin/spring2
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--config/initializers/1_settings.rb8
-rw-r--r--config/initializers/chronic_duration.rb1
-rw-r--r--config/initializers/default_url_options.rb1
-rw-r--r--config/initializers/metrics.rb21
-rw-r--r--config/initializers/sidekiq.rb4
-rw-r--r--config/routes.rb32
-rw-r--r--db/fixtures/development/15_award_emoji.rb33
-rw-r--r--db/migrate/20160314114439_add_requested_at_to_members.rb5
-rw-r--r--db/migrate/20160415062917_create_personal_access_tokens.rb13
-rw-r--r--db/migrate/20160416182152_convert_award_note_to_emoji_award.rb33
-rw-r--r--db/migrate/20160416190505_remove_note_is_award.rb6
-rw-r--r--db/migrate/20160509091049_add_locked_to_ci_runner.rb13
-rw-r--r--db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb5
-rw-r--r--db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb6
-rw-r--r--db/migrate/20160610204157_add_deployments.rb27
-rw-r--r--db/migrate/20160610204158_add_environments.rb17
-rw-r--r--db/migrate/20160610211845_add_environment_to_builds.rb10
-rw-r--r--db/migrate/20160615142710_add_index_on_requested_at_to_members.rb9
-rw-r--r--db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb9
-rw-r--r--db/migrate/20160616084004_change_project_of_environment.rb21
-rw-r--r--db/migrate/20160616102642_remove_duplicated_keys.rb19
-rw-r--r--db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb21
-rw-r--r--db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb13
-rw-r--r--db/migrate/20160617301627_add_events_to_notification_settings.rb (renamed from db/migrate/20160614301627_add_events_to_notification_settings.rb)0
-rw-r--r--db/migrate/20160620115026_add_index_on_runners_locked.rb12
-rw-r--r--db/schema.rb89
-rw-r--r--doc/README.md23
-rw-r--r--doc/administration/container_registry.md86
-rw-r--r--doc/administration/raketasks/project_import_export.md33
-rw-r--r--doc/administration/troubleshooting/sidekiq.md3
-rw-r--r--doc/api/README.md110
-rw-r--r--doc/api/award_emoji.md367
-rw-r--r--doc/api/builds.md528
-rw-r--r--doc/api/ci/README.md24
-rw-r--r--doc/api/ci/builds.md138
-rw-r--r--doc/api/ci/runners.md57
-rw-r--r--doc/api/issues.md3
-rw-r--r--doc/api/sidekiq_metrics.md152
-rw-r--r--doc/ci/README.md4
-rw-r--r--doc/ci/api/README.md21
-rw-r--r--doc/ci/api/builds.md138
-rw-r--r--doc/ci/api/runners.md45
-rw-r--r--doc/ci/docker/using_docker_build.md224
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/ci/environments.md58
-rw-r--r--doc/ci/examples/README.md3
-rw-r--r--doc/ci/examples/php.md4
-rw-r--r--doc/ci/examples/test-scala-application.md47
-rw-r--r--doc/ci/pipelines.md38
-rw-r--r--doc/ci/quick_start/README.md64
-rw-r--r--doc/ci/quick_start/img/pipelines_status.pngbin0 -> 89387 bytes
-rw-r--r--doc/ci/runners/README.md14
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md202
-rw-r--r--doc/container_registry/README.md23
-rw-r--r--doc/development/doc_styleguide.md56
-rw-r--r--doc/development/instrumentation.md22
-rw-r--r--doc/development/migration_style_guide.md16
-rw-r--r--doc/intro/README.md2
-rw-r--r--doc/permissions/permissions.md3
-rw-r--r--doc/project_services/emails_on_push.md17
-rw-r--r--doc/project_services/img/emails_on_push_service.pngbin0 -> 98160 bytes
-rw-r--r--doc/project_services/project_services.md2
-rw-r--r--doc/update/8.6-to-8.7.md2
-rw-r--r--doc/user/project/img/labels_assign_label_in_new_issue.pngbin0 -> 31126 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_sidebar.pngbin0 -> 31537 bytes
-rw-r--r--doc/user/project/img/labels_assign_label_sidebar_saved.pngbin0 -> 28396 bytes
-rw-r--r--doc/user/project/img/labels_default.pngbin0 -> 80403 bytes
-rw-r--r--doc/user/project/img/labels_description_tooltip.pngbin0 -> 22585 bytes
-rw-r--r--doc/user/project/img/labels_filter.pngbin0 -> 81536 bytes
-rw-r--r--doc/user/project/img/labels_filter_by_priority.pngbin0 -> 60849 bytes
-rw-r--r--doc/user/project/img/labels_generate.pngbin0 -> 31608 bytes
-rw-r--r--doc/user/project/img/labels_new_label.pngbin0 -> 43265 bytes
-rw-r--r--doc/user/project/img/labels_new_label_on_the_fly.pngbin0 -> 10416 bytes
-rw-r--r--doc/user/project/img/labels_new_label_on_the_fly_create.pngbin0 -> 16151 bytes
-rw-r--r--doc/user/project/img/labels_prioritize.pngbin0 -> 108751 bytes
-rw-r--r--doc/user/project/img/labels_subscribe.pngbin0 -> 11536 bytes
-rw-r--r--doc/user/project/labels.md147
-rw-r--r--doc/user/project/settings/img/import_export_download_export.pngbin0 -> 85600 bytes
-rw-r--r--doc/user/project/settings/img/import_export_export_button.pngbin0 -> 84637 bytes
-rw-r--r--doc/user/project/settings/img/import_export_mail_link.pngbin0 -> 44012 bytes
-rw-r--r--doc/user/project/settings/img/import_export_new_project.pngbin0 -> 43574 bytes
-rw-r--r--doc/user/project/settings/img/import_export_select_file.pngbin0 -> 46292 bytes
-rw-r--r--doc/user/project/settings/img/settings_edit_button.pngbin0 -> 19392 bytes
-rw-r--r--doc/user/project/settings/import_export.md73
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/add-user/add-user.md24
-rw-r--r--doc/workflow/add-user/img/access_requests_management.pngbin0 -> 15105 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_accept.pngbin10833 -> 22961 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_ready.pngbin16177 -> 40305 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_search.pngbin15889 -> 45884 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_give_permissions.pngbin22089 -> 56480 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_import_members_from_another_project.pngbin18897 -> 38874 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_imported_members.pngbin23897 -> 37873 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_list_members.pngbin15732 -> 24427 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_members_menu.pngbin8295 -> 42319 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_search_people.pngbin13518 -> 39941 bytes
-rw-r--r--doc/workflow/add-user/img/request_access_button.pngbin0 -> 36588 bytes
-rw-r--r--doc/workflow/add-user/img/withdraw_access_request_button.pngbin0 -> 37960 bytes
-rw-r--r--doc/workflow/award_emoji.md45
-rw-r--r--doc/workflow/award_emoji.pngbin6620 -> 16926 bytes
-rw-r--r--doc/workflow/groups.md22
-rw-r--r--doc/workflow/groups/access_requests_management.pngbin0 -> 15193 bytes
-rw-r--r--doc/workflow/groups/request_access_button.pngbin0 -> 30470 bytes
-rw-r--r--doc/workflow/groups/withdraw_access_request_button.pngbin0 -> 31681 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_awarded.pngbin0 -> 64317 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_picker.pngbin0 -> 250861 bytes
-rw-r--r--doc/workflow/labels.md17
-rw-r--r--doc/workflow/labels/label1.pngbin5846 -> 0 bytes
-rw-r--r--doc/workflow/labels/label2.pngbin16931 -> 0 bytes
-rw-r--r--doc/workflow/labels/label3.pngbin19360 -> 0 bytes
-rw-r--r--doc/workflow/notifications.md2
-rw-r--r--doc/workflow/notifications/settings.pngbin88149 -> 90986 bytes
-rw-r--r--doc/workflow/shortcuts.pngbin90936 -> 108255 bytes
-rw-r--r--features/admin/active_tab.feature22
-rw-r--r--features/admin/groups.feature7
-rw-r--r--features/dashboard/group.feature44
-rw-r--r--features/dashboard/new_project.feature2
-rw-r--r--features/dashboard/todos.feature7
-rw-r--r--features/project/active_tab.feature30
-rw-r--r--features/project/shortcuts.feature6
-rw-r--r--features/steps/admin/active_tab.rb36
-rw-r--r--features/steps/admin/groups.rb8
-rw-r--r--features/steps/dashboard/group.rb42
-rw-r--r--features/steps/dashboard/new_project.rb8
-rw-r--r--features/steps/dashboard/todos.rb35
-rw-r--r--features/steps/group/members.rb14
-rw-r--r--features/steps/profile/notifications.rb6
-rw-r--r--features/steps/project/network_graph.rb20
-rw-r--r--features/steps/project/project.rb4
-rw-r--r--features/steps/project/project_find_file.rb4
-rw-r--r--features/steps/project/source/browse_files.rb20
-rw-r--r--features/steps/project/team_management.rb26
-rw-r--r--features/steps/shared/project_tab.rb4
-rw-r--r--lib/api/api.rb48
-rw-r--r--lib/api/award_emoji.rb116
-rw-r--r--lib/api/builds.rb22
-rw-r--r--lib/api/entities.rb27
-rw-r--r--lib/api/gitignores.rb29
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/project_members.rb2
-rw-r--r--lib/api/runners.rb12
-rw-r--r--lib/api/sidekiq_metrics.rb90
-rw-r--r--lib/api/templates.rb36
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb52
-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/relative_link_filter.rb51
-rw-r--r--lib/banzai/filter/upload_link_filter.rb11
-rw-r--r--lib/banzai/filter/wiki_link_filter.rb2
-rw-r--r--lib/banzai/pipeline/description_pipeline.rb17
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb16
-rw-r--r--lib/banzai/renderer.rb8
-rw-r--r--lib/ci/api/builds.rb2
-rw-r--r--lib/ci/api/entities.rb3
-rw-r--r--lib/ci/api/runners.rb9
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb64
-rw-r--r--lib/container_registry/blob.rb2
-rw-r--r--lib/container_registry/client.rb15
-rw-r--r--lib/container_registry/tag.rb15
-rw-r--r--lib/gitlab/access.rb2
-rw-r--r--lib/gitlab/backend/grack_auth.rb9
-rw-r--r--lib/gitlab/backend/shell_env.rb28
-rw-r--r--lib/gitlab/ci/config.rb16
-rw-r--r--lib/gitlab/ci/config/node/configurable.rb61
-rw-r--r--lib/gitlab/ci/config/node/entry.rb77
-rw-r--r--lib/gitlab/ci/config/node/factory.rb39
-rw-r--r--lib/gitlab/ci/config/node/global.rb18
-rw-r--r--lib/gitlab/ci/config/node/null.rb27
-rw-r--r--lib/gitlab/ci/config/node/script.rb29
-rw-r--r--lib/gitlab/ci/config/node/validation_helpers.rb55
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/database.rb9
-rw-r--r--lib/gitlab/database/migration_helpers.rb119
-rw-r--r--lib/gitlab/email/message/repository_push.rb2
-rw-r--r--lib/gitlab/github_import/importer.rb23
-rw-r--r--lib/gitlab/gitignore.rb56
-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/lfs/response.rb7
-rw-r--r--lib/gitlab/lfs/router.rb7
-rw-r--r--lib/gitlab/metrics/instrumentation.rb26
-rw-r--r--lib/gitlab/metrics/method_call.rb52
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb31
-rw-r--r--lib/gitlab/metrics/sampler.rb6
-rw-r--r--lib/gitlab/metrics/transaction.rb35
-rw-r--r--lib/gitlab/regex.rb8
-rw-r--r--lib/gitlab/template/base_template.rb67
-rw-r--r--lib/gitlab/template/gitignore.rb22
-rw-r--r--lib/gitlab/template/gitlab_ci_yml.rb27
-rw-r--r--lib/gitlab/workhorse.rb2
-rw-r--r--lib/tasks/gitlab/import_export.rake13
-rw-r--r--lib/tasks/gitlab/update_gitignore.rake46
-rw-r--r--lib/tasks/gitlab/update_templates.rake54
-rw-r--r--spec/controllers/application_controller_spec.rb71
-rw-r--r--spec/controllers/blob_controller_spec.rb5
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb196
-rw-r--r--spec/controllers/profiles/accounts_controller_spec.rb26
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb12
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb19
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb245
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb102
-rw-r--r--spec/controllers/projects_controller_spec.rb20
-rw-r--r--spec/factories/ci/pipelines.rb (renamed from spec/factories/ci/commits.rb)0
-rw-r--r--spec/factories/deployments.rb13
-rw-r--r--spec/factories/environments.rb7
-rw-r--r--spec/factories/personal_access_tokens.rb9
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/features/admin/admin_hooks_spec.rb4
-rw-r--r--spec/features/admin/admin_runners_spec.rb34
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb51
-rw-r--r--spec/features/builds_spec.rb36
-rw-r--r--spec/features/container_registry_spec.rb3
-rw-r--r--spec/features/environments_spec.rb160
-rw-r--r--spec/features/groups/members/last_owner_cannot_leave_group_spec.rb16
-rw-r--r--spec/features/groups/members/member_leaves_group_spec.rb21
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb48
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb49
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb (renamed from spec/features/issues/bulk_assigment_labels_spec.rb)3
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb20
-rw-r--r--spec/features/issues/filter_issues_spec.rb23
-rw-r--r--spec/features/issues/move_spec.rb16
-rw-r--r--spec/features/issues/todo_spec.rb43
-rw-r--r--spec/features/issues_spec.rb47
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb94
-rw-r--r--spec/features/projects/badges/list_spec.rb8
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb30
-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/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb50
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb21
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb47
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb19
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb16
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb55
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/features/search_spec.rb79
-rw-r--r--spec/features/security/project/public_access_spec.rb43
-rw-r--r--spec/features/todos/todos_spec.rb4
-rw-r--r--spec/features/u2f_spec.rb57
-rw-r--r--spec/finders/notes_finder_spec.rb23
-rw-r--r--spec/fixtures/container_registry/tag_manifest.json17
-rw-r--r--spec/fixtures/container_registry/tag_manifest_1.json32
-rw-r--r--spec/helpers/application_helper_spec.rb45
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb79
-rw-r--r--spec/helpers/issues_helper_spec.rb16
-rw-r--r--spec/helpers/members_helper_spec.rb104
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb6
-rw-r--r--spec/helpers/projects_helper_spec.rb10
-rw-r--r--spec/javascripts/application_spec.js.coffee30
-rw-r--r--spec/javascripts/fixtures/application.html.haml2
-rw-r--r--spec/javascripts/fixtures/search_autocomplete.html.haml10
-rw-r--r--spec/javascripts/fixtures/u2f/register.html.haml3
-rw-r--r--spec/javascripts/issue_spec.js.coffee13
-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.rb15
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb20
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb26
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb205
-rw-r--r--spec/lib/container_registry/repository_spec.rb2
-rw-r--r--spec/lib/container_registry/tag_spec.rb89
-rw-r--r--spec/lib/gitlab/ci/config/node/configurable_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb104
-rw-r--r--spec/lib/gitlab/ci/config/node/null_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/config/node/script_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb42
-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/lfs/lfs_router_spec.rb561
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb66
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb91
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb45
-rw-r--r--spec/lib/gitlab/metrics/sampler_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb16
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/search_results_spec.rb16
-rw-r--r--spec/lib/gitlab/template/gitignore_spec.rb (renamed from spec/lib/gitlab/gitignore_spec.rb)6
-rw-r--r--spec/mailers/notify_spec.rb297
-rw-r--r--spec/mailers/previews/devise_mailer_preview.rb23
-rw-r--r--spec/models/build_spec.rb197
-rw-r--r--spec/models/ci/pipeline_spec.rb13
-rw-r--r--spec/models/ci/runner_spec.rb241
-rw-r--r--spec/models/commit_spec.rb12
-rw-r--r--spec/models/commit_status_spec.rb19
-rw-r--r--spec/models/concerns/access_requestable_spec.rb40
-rw-r--r--spec/models/concerns/milestoneish_spec.rb14
-rw-r--r--spec/models/concerns/participable_spec.rb10
-rw-r--r--spec/models/deployment_spec.rb17
-rw-r--r--spec/models/environment_spec.rb14
-rw-r--r--spec/models/event_spec.rb6
-rw-r--r--spec/models/group_spec.rb60
-rw-r--r--spec/models/jira_issue_spec.rb30
-rw-r--r--spec/models/key_spec.rb2
-rw-r--r--spec/models/member_spec.rb112
-rw-r--r--spec/models/members/group_member_spec.rb18
-rw-r--r--spec/models/members/project_member_spec.rb18
-rw-r--r--spec/models/note_spec.rb15
-rw-r--r--spec/models/personal_access_token_spec.rb15
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb12
-rw-r--r--spec/models/project_services/jira_service_spec.rb6
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb10
-rw-r--r--spec/models/project_spec.rb50
-rw-r--r--spec/models/project_team_spec.rb144
-rw-r--r--spec/models/repository_spec.rb41
-rw-r--r--spec/requests/api/api_helpers_spec.rb76
-rw-r--r--spec/requests/api/award_emoji_spec.rb198
-rw-r--r--spec/requests/api/builds_spec.rb37
-rw-r--r--spec/requests/api/gitignores_spec.rb29
-rw-r--r--spec/requests/api/issues_spec.rb25
-rw-r--r--spec/requests/api/milestones_spec.rb13
-rw-r--r--spec/requests/api/projects_spec.rb16
-rw-r--r--spec/requests/api/runners_spec.rb24
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb40
-rw-r--r--spec/requests/api/templates_spec.rb52
-rw-r--r--spec/requests/ci/api/builds_spec.rb36
-rw-r--r--spec/requests/git_http_spec.rb2
-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/create_deployment_service_spec.rb119
-rw-r--r--spec/services/git_push_service_spec.rb3
-rw-r--r--spec/services/members/destroy_service_spec.rb71
-rw-r--r--spec/services/notification_service_spec.rb18
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb12
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/todo_service_spec.rb140
-rw-r--r--spec/support/import_export/import_export.yml20
-rw-r--r--spec/support/test_env.rb1
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb69
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb29
-rw-r--r--spec/workers/stuck_ci_builds_worker_spec.rb19
-rw-r--r--vendor/assets/javascripts/raphael.js8239
-rw-r--r--vendor/gitignore/Android.gitignore3
-rw-r--r--vendor/gitignore/C++.gitignore1
-rw-r--r--vendor/gitignore/CMake.gitignore1
-rw-r--r--vendor/gitignore/D.gitignore4
-rw-r--r--vendor/gitignore/Global/Bazaar.gitignore2
-rw-r--r--vendor/gitignore/Global/OSX.gitignore3
-rw-r--r--vendor/gitignore/Global/README.md10
-rw-r--r--vendor/gitignore/Global/SublimeText.gitignore13
-rw-r--r--vendor/gitignore/Haskell.gitignore1
-rw-r--r--vendor/gitignore/Julia.gitignore4
-rw-r--r--vendor/gitignore/LICENSE19
-rw-r--r--vendor/gitignore/Laravel.gitignore1
-rw-r--r--vendor/gitignore/Objective-C.gitignore9
-rw-r--r--vendor/gitignore/Qt.gitignore2
-rw-r--r--vendor/gitignore/README.md14
-rw-r--r--vendor/gitignore/Rails.gitignore4
-rw-r--r--vendor/gitignore/Swift.gitignore2
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore1
-rw-r--r--vendor/gitignore/VisualStudio.gitignore1
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml18
-rw-r--r--vendor/gitlab-ci-yml/LICENSE21
-rw-r--r--vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml27
-rw-r--r--vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml16
-rw-r--r--vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml13
-rw-r--r--vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml16
-rw-r--r--vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml25
-rw-r--r--vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml11
-rw-r--r--vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml25
-rw-r--r--vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml24
-rw-r--r--vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml17
-rw-r--r--vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml27
-rw-r--r--vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml15
-rw-r--r--vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml10
-rw-r--r--vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml30
791 files changed, 30411 insertions, 5176 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f1dcf990629..219077d79b8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -92,9 +92,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- - knapsack spinach "-r rerun"
- # retry failed tests 3 times
- - retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)'
+ - knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
paths:
- knapsack/
@@ -131,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 65f9043e91b..aef536b2a8e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,16 +3,30 @@ v 8.10(unreleased)
- Add notifications dropdown for groups
v 8.9.0 (unreleased)
+ - Fix builds API response not including commit data
+ - 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.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
+ - Show Star and Fork buttons on mobile.
+ - Performance improvements on RelativeLinkFilter
- Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
+ - Fix MR-auto-close text added to description
- Fix issue with arrow keys not working in search autocomplete dropdown
+ - Fix an issue where note polling stopped working if a window was in the
+ background during a refresh.
+ - Pre-processing Markdown now only happens when needed
- 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
+ - Add a border around images to differentiate them from the background.
- Don't show tags for revert and cherry-pick operations
+ - Show image ID on registry page
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
- Allow customisable text on the 'nearly there' page after a user signs up
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
@@ -21,43 +35,74 @@ 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
+ - Add number of merge requests for a given milestone to the milestones view.
+ - 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
+ - Wrap code blocks on Activies and Todos page !4783 (winniehell)
+ - 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
+ - Support Docker Registry manifest v1
- `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
+ - Refactoring Award Emoji with API support for Issues and MergeRequests
- Use Knapsack to evenly distribute tests across multiple nodes
- - Add notifications dropdown for groups
- 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
+ - Limit email on push diff size to 30 files / 150 KB
- 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
+ - Disable SAML account unlink feature
- 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
+ - POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project
- 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
- - Customized notification settings for projects
+ - Allow to use CI token to fetch LFS objects
+ - Custom notification settings
- Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails
+ - Added Gfm autocomplete for labels
+ - Added edit note 'up' shortcut documentation to the help panel and docs screenshot #18114
- 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
+ - Make it possible to lock a runner from being enabled for other projects
- Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav
- Use Knapsack only in CI environment
- Cache project build count in sidebar nav
- Add milestone expire date to the right sidebar
+ - Manually mark a issue or merge request as a todo
- Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
- Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects
@@ -67,27 +112,66 @@ 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
+ - Dropdown for `.gitlab-ci.yml` templates
- 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
- Toggling a task list item in a issue/mr description does not creates a Todo for mentions
- 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
-
-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
+ - Remove deprecated issues_tracker and issues_tracker_id from project model
+ - Allow users to create confidential issues in private projects
+ - Measure CPU time for instrumented methods
+ - Instrument private methods and private instance methods by default instead just public methods
+ - Only show notes through JSON on confidential issues that the user has access to
+ - Updated the allocations Gem to version 1.0.5
+ - The background sampler now ignores classes without names
+ - Update design for `Close` buttons
+ - New custom icons for navigation
+ - Horizontally scrolling navigation on project, group, and profile settings pages
+ - Hide global side navigation by default
+ - Fix project Star/Unstar project button tooltip
+ - 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
+ - Set inverse_of for Project/Service association to reduce the number of queries
+ - Update tanuki logo highlight/loading colors
+ - Remove explicit Gitlab::Metrics.action assignments, are already automatic.
+ - Use Git cached counters for branches and tags on project page
+ - Cache participable participants in an instance variable.
+ - Filter parameters for request_uri value on instrumented transactions.
+ - Remove duplicated keys add UNIQUE index to keys fingerprint column
+ - ExtractsPath get ref_names from repository cache, if not there access git.
+ - Cache user todo counts from TodoService
+ - Ensure Todos counters doesn't count Todos for projects pending delete
+ - Add left/right arrows horizontal navigation
+ - Add tooltip to pin/unpin navbar
+ - Add new sub nav style to Wiki and Graphs sub navigation
+
+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
@@ -203,6 +287,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)
@@ -365,6 +452,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
@@ -519,6 +611,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
@@ -680,6 +776,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
@@ -816,6 +916,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
@@ -934,6 +1038,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 6d8a33c2eef..092ea9d69b0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -48,11 +48,11 @@ gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
# Browser detection
-gem "browser", '~> 2.0.3'
+gem "browser", '~> 2.2'
# 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
@@ -210,6 +210,9 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
+# Parse duration
+gem 'chronic_duration', '~> 0.10.6'
+
gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2'
@@ -218,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'
@@ -328,7 +330,7 @@ gem "newrelic_rpm", '~> 3.14'
gem 'octokit', '~> 4.3.0'
-gem "mail_room", "~> 0.7"
+gem "mail_room", "~> 0.8"
gem 'email_reply_parser', '~> 0.5.8'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2ba2676efa1..ba16e4bf337 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -50,7 +50,7 @@ GEM
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0)
- allocations (1.0.4)
+ allocations (1.0.5)
arel (6.0.3)
asana (0.4.0)
faraday (~> 0.9)
@@ -98,7 +98,7 @@ GEM
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
brakeman (3.3.2)
- browser (2.0.3)
+ browser (2.2.0)
builder (3.2.2)
bullet (5.0.0)
activesupport (>= 3.0.0)
@@ -124,6 +124,8 @@ GEM
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.3)
+ chronic_duration (0.10.6)
+ numerizer (~> 0.1.1)
chunky_png (1.3.5)
cliver (0.3.2)
coderay (1.1.0)
@@ -244,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)
@@ -275,7 +277,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (10.1.0)
+ gitlab_git (10.2.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -396,9 +398,9 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.7.0)
+ mail_room (0.8.0)
method_source (0.8.2)
- mime-types (2.99.1)
+ mime-types (2.99.2)
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
@@ -414,6 +416,7 @@ GEM
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
+ numerizer (0.1.1)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
@@ -553,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)
@@ -831,7 +833,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.3.0)
- browser (~> 2.0.3)
+ browser (~> 2.2)
bullet
bundler-audit
byebug
@@ -839,6 +841,7 @@ DEPENDENCIES
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.10.0)
charlock_holmes (~> 0.7.3)
+ chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
@@ -863,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)
@@ -871,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)
@@ -896,7 +899,7 @@ DEPENDENCIES
license_finder
licensee (~> 8.0.0)
loofah (~> 2.0.3)
- mail_room (~> 0.7)
+ mail_room (~> 0.8)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
@@ -934,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..6d8faba40d7 100644
--- a/app/assets/javascripts/LabelManager.js.coffee
+++ b/app/assets/javascripts/LabelManager.js.coffee
@@ -27,6 +27,11 @@ class @LabelManager
$btn = $(e.currentTarget)
$label = $("##{$btn.data('domId')}")
action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
+
+ # Make sure tooltip will hide
+ $tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}"
+ $tooltip.tooltip 'destroy'
+
_this.toggleLabelPriority($label, action)
toggleLabelPriority: ($label, action, persistState = true) ->
@@ -42,10 +47,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 +59,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/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 3f61ea1eaf4..cf46f15a156 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -7,6 +7,7 @@
labelsPath: "/api/:version/projects/:id/labels"
licensePath: "/api/:version/licenses/:key"
gitignorePath: "/api/:version/gitignores/:key"
+ gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key"
group: (group_id, callback) ->
url = Api.buildUrl(Api.groupPath)
@@ -110,6 +111,12 @@
$.get url, (gitignore) ->
callback(gitignore)
+ gitlabCiYml: (key, callback) ->
+ url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key)
+
+ $.get url, (file) ->
+ callback(file)
+
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 69d4c4f5dd3..0206db461da 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
@@ -125,9 +121,15 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+
+ $document = $(document)
+ $window = $(window)
+ $body = $('body')
+
+ 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", ->
@@ -155,7 +157,7 @@ $ ->
), 1
# Initialize tooltips
- $('body').tooltip(
+ $body.tooltip(
selector: '.has-tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
@@ -174,7 +176,7 @@ $ ->
flash.show()
# Disable form buttons while a form is submitting
- $('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
+ $body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
buttons = $('[type="submit"]', @)
switch e.type
@@ -187,7 +189,7 @@ $ ->
$('.account-box').hover -> $(@).toggleClass('hover')
# Commit show suppressed diff
- $(document).on 'click', '.diff-content .js-show-suppressed-diff', ->
+ $document.on 'click', '.diff-content .js-show-suppressed-diff', ->
$container = $(@).parent()
$container.next('table').show()
$container.remove()
@@ -200,13 +202,13 @@ $ ->
$('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left")
# Show/hide comments on diff
- $("body").on "click", ".js-toggle-diff-comments", (e) ->
+ $body.on "click", ".js-toggle-diff-comments", (e) ->
$(@).toggleClass('active')
$(@).closest(".diff-file").find(".notes_holder").toggle()
e.preventDefault()
- $(document).off "click", '.js-confirm-danger'
- $(document).on "click", '.js-confirm-danger', (e) ->
+ $document.off "click", '.js-confirm-danger'
+ $document.on "click", '.js-confirm-danger', (e) ->
e.preventDefault()
btn = $(e.target)
text = btn.data("confirm-danger-message")
@@ -214,7 +216,7 @@ $ ->
new ConfirmDangerModal(form, text)
- $(document).on 'click', 'button', ->
+ $document.on 'click', 'button', ->
$(this).blur()
$('input[type="search"]').each ->
@@ -222,7 +224,7 @@ $ ->
$this.attr 'value', $this.val()
return
- $(document)
+ $document
.off 'keyup', 'input[type="search"]'
.on 'keyup', 'input[type="search"]' , (e) ->
$this = $(this)
@@ -230,7 +232,7 @@ $ ->
$sidebarGutterToggle = $('.js-sidebar-toggle')
- $(document)
+ $document
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
@@ -242,14 +244,14 @@ $ ->
oldBootstrapBreakpoint = bootstrapBreakpoint
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
- $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+ $document.trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
- $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+ $document.trigger('breakpoint:change', [bootstrapBreakpoint])
- $(window)
+ $window
.off "resize.app"
.on "resize.app", (e) ->
fitSidebarForSize()
@@ -257,3 +259,47 @@ $ ->
gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize()
new Aside()
+
+ # Sidenav pinning
+ if $window.width() < 1440 and $.cookie('pin_nav') is 'true'
+ $.cookie('pin_nav', 'false', { path: '/' })
+ $('.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()
+
+ $pinBtn = $(e.currentTarget)
+ $page = $ '.page-with-sidebar'
+ $topNav = $ '.navbar-fixed-top'
+ $tooltip = $ "##{$pinBtn.attr('aria-describedby')}"
+ doPinNav = not $page.is('.page-sidebar-pinned')
+ tooltipText = 'Pin navigation'
+
+ $(this).toggleClass 'is-active'
+
+ if doPinNav
+ $page.addClass('page-sidebar-pinned')
+ $topNav.addClass('header-pinned-nav')
+ else
+ $tooltip.remove() # Remove it immediately when collapsing the sidebar
+ $page.removeClass('page-sidebar-pinned')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+ $topNav.removeClass('header-pinned-nav')
+ .toggleClass('header-collapsed header-expanded')
+
+ # Save settings
+ $.cookie 'pin_nav', doPinNav, { path: '/' }
+
+ if $.cookie('pin_nav') is 'true' or doPinNav
+ tooltipText = 'Unpin navigation'
+
+ # Update tooltip text immediately
+ $tooltip.find('.tooltip-inner').text(tooltipText)
+
+ # Persist tooltip title
+ $pinBtn.attr('title', tooltipText).tooltip('fixTitle')
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_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
new file mode 100644
index 00000000000..d9a03d05529
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
@@ -0,0 +1,23 @@
+#= require blob/template_selector
+
+class @BlobCiYamlSelector extends TemplateSelector
+ requestFile: (query) ->
+ Api.gitlabCiYml query.name, @requestFileSuccess.bind(@)
+
+class @BlobCiYamlSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-gitlab-ci-yml-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobCiYamlSelector(
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
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..19e584519d7 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -12,8 +12,10 @@ class @EditBlob
$("#file-content").val(@editor.getValue())
@initModePanesAndLinks()
- new BlobLicenseSelector(@editor)
- new BlobGitignoreSelectors(editor: @editor)
+
+ new BlobLicenseSelectors { @editor }
+ new BlobGitignoreSelectors { @editor }
+ new BlobCiYamlSelectors { @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/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index f763ba96e33..2d515d7efa2 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -17,6 +17,8 @@ class @CiBuild
.off 'resize.build'
.on 'resize.build', @hideSidebar
+ @updateArtifactRemoveDate()
+
if $('#build-trace').length
@getInitialBuildTrace()
@initScrollButtonAffix()
@@ -103,3 +105,10 @@ class @CiBuild
$('.js-build-sidebar')
.removeClass 'right-sidebar-collapsed'
.addClass 'right-sidebar-expanded'
+
+ updateArtifactRemoveDate: ->
+ $date = $('.js-artifacts-remove')
+
+ if $date.length
+ date = $date.text()
+ $date.text $.timefor(new Date(date), ' ')
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index a4a309810c5..5bb9647f6c2 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()
@@ -53,9 +54,13 @@ class Dispatcher
new Diff()
shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
+ new MergedButtons()
+ when 'projects:merge_requests:commits', 'projects:merge_requests:builds'
+ new MergedButtons()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
+ new MergedButtons()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
Issuable.init()
@@ -68,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()
@@ -98,6 +101,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,14 +137,13 @@ 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'
new ProjectNew()
when 'show'
+ new ProjectNew()
new ProjectShow()
new NotificationsDropdown()
when 'wikis'
@@ -151,9 +154,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/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index b49bd4565a7..2a7bf0bc306 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -58,7 +58,7 @@ class GitLabDropdownFilter
filter: (search_text) ->
data = @options.data()
- if data?
+ if data? and not @options.filterByText
results = data
if search_text isnt ''
@@ -102,10 +102,11 @@ class GitLabDropdownFilter
$el = $(@)
matches = fuzzaldrinPlus.match($el.text().trim(), search_text)
- if matches.length
- $el.show()
- else
- $el.hide()
+ unless $el.is('.dropdown-header')
+ if matches.length
+ $el.show()
+ else
+ $el.hide()
else
elements.show()
@@ -191,6 +192,7 @@ class GitLabDropdown
if @options.filterable
@filter = new GitLabDropdownFilter @filterInput,
filterInputBlur: @filterInputBlur
+ filterByText: @options.filterByText
remote: @options.filterRemote
query: @options.data
keys: searchFields
@@ -302,6 +304,9 @@ class GitLabDropdown
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
+ if @options.setActiveIds
+ @options.setActiveIds.call(@)
+
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee
index d540cc4dc46..77512d187c9 100644
--- a/app/assets/javascripts/gl_form.js.coffee
+++ b/app/assets/javascripts/gl_form.js.coffee
@@ -34,6 +34,8 @@ class @GLForm
# form and textarea event listeners
@addEventListeners()
+ gl.text.init(@form)
+
# hide discard button
@form.find('.js-note-discard').hide()
@@ -42,6 +44,7 @@ class @GLForm
clearEventListeners: ->
@textarea.off 'focus'
@textarea.off 'blur'
+ gl.text.removeListeners(@form)
addEventListeners: ->
@textarea.on 'focus', ->
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
index 584d281a510..834a81af459 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
@@ -121,7 +121,11 @@ class @ContributorsMasterGraph extends ContributorsGraph
class @ContributorsAuthorGraph extends ContributorsGraph
constructor: (@data) ->
- @width = $('.content').width()/2 - 100
+ # Don't split graph size in half for mobile devices.
+ if $(window).width() < 768
+ @width = $('.content').width() - 80
+ else
+ @width = ($('.content').width() / 2) - 100
@height = 200
@x = null
@y = null
diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee
index c2447120033..d0901be1509 100644
--- a/app/assets/javascripts/issuable.js.coffee
+++ b/app/assets/javascripts/issuable.js.coffee
@@ -56,13 +56,6 @@ issuable_created = false
Issuable.filterResults $('.filter-form')
$('.js-label-select').trigger('update.label')
- toggleLabelFilters: ->
- $filteredLabels = $('.filtered-labels')
- if $filteredLabels.find('.label-row').length > 0
- $filteredLabels.removeClass('hidden')
- else
- $filteredLabels.addClass('hidden')
-
filterResults: (form) =>
formData = form.serialize()
@@ -71,58 +64,16 @@ issuable_created = false
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData
- $.ajax
- type: 'GET'
- url: formAction
- data: formData
- complete: ->
- $('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
- success: (data) ->
- $('.issues-holder, .merge-requests-holder').html(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: issuesUrl}, document.title, issuesUrl
- Issuable.reload()
- Issuable.updateStateFilters()
- $filteredLabels = $('.filtered-labels')
-
- if typeof Issuable.labelRow is 'function'
- $filteredLabels.html(Issuable.labelRow(data))
- Issuable.toggleLabelFilters()
-
- dataType: "json"
-
- reload: ->
- if Issuable.created
- Issuable.initChecks()
-
- $('#filter_issue_search').val($('#issue_search').val())
+ Turbolinks.visit(issuesUrl);
initChecks: ->
- $('.check_all_issues').on 'click', ->
+ $('.check_all_issues').off('click').on('click', ->
$('.selected_issue').prop('checked', @checked)
Issuable.checkChanged()
+ )
- $('.selected_issue').on 'change', Issuable.checkChanged
-
- updateStateFilters: ->
- stateFilters = $('.issues-state-filters, .dropdown-menu-sort')
- newParams = {}
- paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search']
-
- for paramKey in paramKeys
- newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
-
- if stateFilters.length
- stateFilters.find('a').each ->
- initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
- labelNameValues = gl.utils.getParameterValues('label_name[]')
- if labelNameValues
- labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
- newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
- else
- newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
- $(this).attr 'href', newUrl
+ $('.selected_issue').off('change').on('change', Issuable.checkChanged)
checkChanged: ->
checked_issues = $('.selected_issue:checked')
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
index 898506fde32..5b7a4831dfc 100644
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ b/app/assets/javascripts/issuable_form.js.coffee
@@ -102,6 +102,10 @@ class @IssuableForm
return {
results: data
}
+ data: (query) ->
+ {
+ search: query
+ }
formatResult: (project) ->
project.name_with_namespace
formatSelection: (project) ->
diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee
index 9dc3529a17f..b454f9389dd 100644
--- a/app/assets/javascripts/issues-bulk-assignment.js.coffee
+++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee
@@ -9,6 +9,9 @@ class @IssuableBulkActions
@bindEvents()
+ # Fixes bulk-assign not working when navigating through pages
+ Issuable.initChecks();
+
getElement: (selector) ->
@container.find selector
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 9ca88f1226e..6a10db10eb1 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)
@@ -210,9 +210,21 @@ class @LabelsSelect
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
+ active = instance.activeIds
+
if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate'
+ if active.indexOf(label.id) isnt -1
+ # Remove is-indeterminate class if the item will be marked as active
+ i = selectedClass.indexOf 'is-indeterminate'
+ selectedClass.splice i, 1 unless i is -1
+
+ selectedClass.push 'is-active'
+
+ # Add input manually
+ instance.addInput @fieldName, label.id
+
if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
@@ -328,6 +340,10 @@ class @LabelsSelect
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
+
+ setActiveIds: ->
+ if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @activeIds = _this.getActiveIds()
)
@bindEvents()
@@ -352,3 +368,12 @@ class @LabelsSelect
label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids)
+
+ getActiveIds: ->
+ label_ids = []
+
+ $('.selected_issue:checked').each (i, el) ->
+ issue_id = $(el).data('id')
+ label_ids.push $("#issue_#{issue_id}").data('labels')
+
+ _.intersection.apply _, label_ids
diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee
index 6adac6dac97..f8f0aea427e 100644
--- a/app/assets/javascripts/layout_nav.js.coffee
+++ b/app/assets/javascripts/layout_nav.js.coffee
@@ -1,14 +1,25 @@
-class @LayoutNav
- $ ->
- $('.fade-left').addClass('end-scroll')
- $('.scrolling-tabs').on 'scroll', (event) ->
- $this = $(this)
- $el = $(event.target)
- currentPosition = $this.scrollLeft()
- size = bp.getBreakpointSize()
- controlBtnWidth = $('.controls').width()
- maxPosition = $this.get(0).scrollWidth - $this.parent().width()
- maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length
-
- $el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
- $el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
+hideEndFade = ($scrollingTabs) ->
+ $scrollingTabs.each ->
+ $this = $(@)
+
+ $this
+ .find('.fade-right')
+ .toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth'))
+
+$ ->
+ $('.fade-left').addClass('end-scroll')
+
+ hideEndFade($('.scrolling-tabs'))
+
+ $(window)
+ .off 'resize.nav'
+ .on 'resize.nav', ->
+ hideEndFade($('.scrolling-tabs'))
+
+ $('.scrolling-tabs').on 'scroll', (event) ->
+ $this = $(this)
+ currentPosition = $this.scrollLeft()
+ maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
+
+ $this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0)
+ $this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition)
diff --git a/app/assets/javascripts/lib/common_utils.js.coffee b/app/assets/javascripts/lib/common_utils.js.coffee
index 0000e99a650..e39dcb2daa9 100644
--- a/app/assets/javascripts/lib/common_utils.js.coffee
+++ b/app/assets/javascripts/lib/common_utils.js.coffee
@@ -1,5 +1,46 @@
((w) ->
+ w.gl or= {}
+ w.gl.utils or= {}
+
+ w.gl.utils.isInGroupsPage = ->
+
+ return $('body').data('page').split(':')[0] is 'groups'
+
+
+ w.gl.utils.isInProjectPage = ->
+
+ return $('body').data('page').split(':')[0] is 'projects'
+
+
+ w.gl.utils.getProjectSlug = ->
+
+ return if @isInProjectPage() then $('body').data 'project' else null
+
+
+ w.gl.utils.getGroupSlug = ->
+
+ return if @isInGroupsPage() then $('body').data 'group' else null
+
+
+
+ gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
+
+ $tooltipEl
+ .tooltip 'destroy'
+ .attr 'title', newTitle
+ .tooltip 'fixTitle'
+
+
+ gl.utils.preventDisabledButtons = ->
+
+ $('.btn').click (e) ->
+ if $(this).hasClass 'disabled'
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ return false
+
+
jQuery.timefor = (time, suffix, expiredLabel) ->
return '' unless time
diff --git a/app/assets/javascripts/lib/text_utility.js.coffee b/app/assets/javascripts/lib/text_utility.js.coffee
new file mode 100644
index 00000000000..bb2772dfed2
--- /dev/null
+++ b/app/assets/javascripts/lib/text_utility.js.coffee
@@ -0,0 +1,79 @@
+((w) ->
+ w.gl ?= {}
+ w.gl.text ?= {}
+
+ gl.text.randomString = -> Math.random().toString(36).substring(7)
+
+ gl.text.replaceRange = (s, start, end, substitute) ->
+ s.substring(0, start) + substitute + s.substring(end);
+
+ gl.text.selectedText = (text, textarea) ->
+ text.substring(textarea.selectionStart, textarea.selectionEnd)
+
+ gl.text.insertText = (textArea, text, tag, selected, wrap) ->
+ selectedSplit = selected.split('\n')
+ startChar = if not wrap and textArea.selectionStart > 0 then '\n' else ''
+
+ if selectedSplit.length > 1 and not wrap
+ insertText = selectedSplit.map((val) ->
+ if val.indexOf(tag) is 0
+ "#{val.replace(tag, '')}"
+ else
+ "#{tag}#{val}"
+ ).join('\n')
+ else
+ insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}"
+
+ if document.queryCommandSupported('insertText')
+ document.execCommand 'insertText', false, insertText
+ else
+ try
+ document.execCommand("ms-beginUndoUnit")
+
+ textArea.value = @replaceRange(
+ text,
+ textArea.selectionStart,
+ textArea.selectionEnd,
+ insertText)
+ try
+ document.execCommand("ms-endUndoUnit")
+
+ @moveCursor(textArea, tag, wrap)
+
+ gl.text.moveCursor = (textArea, tag, wrapped) ->
+ return unless textArea.setSelectionRange
+
+ if textArea.selectionStart is textArea.selectionEnd
+ if wrapped
+ pos = textArea.selectionStart - tag.length
+ else
+ pos = textArea.selectionStart
+
+ textArea.setSelectionRange pos, pos
+
+ gl.text.updateText = (textArea, tag, wrap) ->
+ $textArea = $(textArea)
+ oldVal = $textArea.val()
+ textArea = $textArea.get(0)
+ text = $textArea.val()
+ selected = @selectedText(text, textArea)
+ $textArea.focus()
+
+ @insertText(textArea, text, tag, selected, wrap)
+
+ gl.text.init = (form) ->
+ self = @
+ $('.js-md', form)
+ .off 'click'
+ .on 'click', ->
+ $this = $(@)
+ self.updateText(
+ $this.closest('.md-area').find('textarea'),
+ $this.data('md-tag'),
+ not $this.data('md-prepend')
+ )
+
+ gl.text.removeListeners = (form) ->
+ $('.js-md', form).off()
+
+) window
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
index 9fdc27a9787..dc2590a0355 100644
--- a/app/assets/javascripts/logo.js.coffee
+++ b/app/assets/javascripts/logo.js.coffee
@@ -42,9 +42,3 @@ work = ->
$(document).on('page:fetch', start)
$(document).on('page:change', stop)
-
-$ ->
- # Make logo clickable as part of a workaround for Safari visited
- # link behaviour (See !2690).
- $('#logo').on 'click', ->
- Turbolinks.visit('/')
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/merged_buttons.js.coffee b/app/assets/javascripts/merged_buttons.js.coffee
new file mode 100644
index 00000000000..4929295c10b
--- /dev/null
+++ b/app/assets/javascripts/merged_buttons.js.coffee
@@ -0,0 +1,30 @@
+class @MergedButtons
+ constructor: ->
+ @$removeBranchWidget = $('.remove_source_branch_widget')
+ @$removeBranchProgress = $('.remove_source_branch_in_progress')
+ @$removeBranchFailed = $('.remove_source_branch_widget.failed')
+
+ @cleanEventListeners()
+ @initEventListeners()
+
+ cleanEventListeners: ->
+ $(document).off 'click', '.remove_source_branch'
+ $(document).off 'ajax:success', '.remove_source_branch'
+ $(document).off 'ajax:error', '.remove_source_branch'
+
+ initEventListeners: ->
+ $(document).on 'click', '.remove_source_branch', @removeSourceBranch
+ $(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess
+ $(document).on 'ajax:error', '.remove_source_branch', @removeBranchError
+
+ removeSourceBranch: =>
+ @$removeBranchWidget.hide()
+ @$removeBranchProgress.show()
+
+ removeBranchSuccess: ->
+ location.reload()
+
+ removeBranchError: ->
+ @$removeBranchWidget.hide()
+ @$removeBranchProgress.hide()
+ @$removeBranchFailed.show()
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 648e1f3bde0..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">
@@ -116,7 +112,7 @@ class @MilestoneSelect
.val()
data = {}
data[abilityName] = {}
- data[abilityName].milestone_id = selected
+ data[abilityName].milestone_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
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/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index ad216910c8d..17f7e180127 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -102,12 +102,15 @@ class @Notes
keydownNoteText: (e) ->
$this = $(this)
- if $this.val() is '' and e.which is 38 #aka the up key
+ if $this.val() is '' and e.which is 38 and not isMetaKey e
myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
if myLastNote.length
myLastNoteEditBtn = myLastNote.find('.js-note-edit')
myLastNoteEditBtn.trigger('click', [true, myLastNote])
+ isMetaKey = (e) ->
+ (e.metaKey or e.ctrlKey or e.altKey or e.shiftKey)
+
initRefresh: ->
clearInterval(Notes.interval)
Notes.interval = setInterval =>
@@ -115,12 +118,14 @@ class @Notes
, @pollingInterval
refresh: =>
- return if @refreshing is true
- @refreshing = true
if not document.hidden and document.URL.indexOf(@noteable_url) is 0
@getContent()
getContent: ->
+ return if @refreshing
+
+ @refreshing = true
+
$.ajax
url: @notes_url
data: "last_fetched_at=" + @last_fetched_at
diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee
index 15daf027c0a..0bbd082c156 100644
--- a/app/assets/javascripts/notifications_dropdown.js.coffee
+++ b/app/assets/javascripts/notifications_dropdown.js.coffee
@@ -1,21 +1,25 @@
class @NotificationsDropdown
- $ ->
+ constructor: ->
$(document)
.off 'click', '.update-notification'
.on 'click', '.update-notification', (e) ->
e.preventDefault()
+
+ return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom'
+
notificationLevel = $(@).data 'notification-level'
label = $(@).data 'notification-title'
- form = $(this).parents('form:first')
+ form = $(this).parents('.notification-form:first')
form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner'
form.find('#notification_setting_level').val(notificationLevel)
- form.submit();
+ form.submit()
$(document)
- .off 'ajax:success', '#notification-form'
- .on 'ajax:success', '#notification-form', (e, data) ->
+ .off 'ajax:success', '.notification-form'
+ .on 'ajax:success', '.notification-form', (e, data) ->
if data.saved
- new Flash('Notification settings saved', 'notice')
- $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html)
+ $(e.currentTarget)
+ .closest('.notification-dropdown')
+ .replaceWith(data.html)
else
new Flash('Failed to save new settings', 'alert')
diff --git a/app/assets/javascripts/notifications_form.js.coffee b/app/assets/javascripts/notifications_form.js.coffee
index 51faa5fcace..3432428702a 100644
--- a/app/assets/javascripts/notifications_form.js.coffee
+++ b/app/assets/javascripts/notifications_form.js.coffee
@@ -12,7 +12,6 @@ class @NotificationsForm
toggleCheckbox: (e) =>
$checkbox = $(e.currentTarget)
$parent = $checkbox.closest('.checkbox')
- console.log($parent)
@saveEvent($checkbox, $parent)
showCheckboxLoadingSpinner: ($parent) ->
@@ -25,13 +24,13 @@ class @NotificationsForm
saveEvent: ($checkbox, $parent) ->
form = $parent.parents('form:first')
- console.log(form)
$.ajax(
url: form.attr('action')
method: form.attr('method')
dataType: 'json'
data: form.serialize()
+
beforeSend: =>
@showCheckboxLoadingSpinner($parent)
).done (data) ->
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index d12bad97a05..96e10dd7e8a 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -19,6 +19,7 @@ class @Project
$('.clone').text(url)
# Ref switcher
+ @initRefSwitcher()
$('.project-refs-select').on 'change', ->
$(@).parents('form').submit()
@@ -34,7 +35,6 @@ class @Project
$(@).parents('.no-password-message').remove()
e.preventDefault()
-
@projectSelectDropdown()
projectSelectDropdown: ->
@@ -50,3 +50,39 @@ class @Project
changeProject: (url) ->
window.location = url
+
+ initRefSwitcher: ->
+ $('.js-project-refs-dropdown').each ->
+ $dropdown = $(@)
+ selected = $dropdown.data('selected')
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: $dropdown.data('refs-url')
+ data:
+ ref: $dropdown.data('ref')
+ ).done (refs) ->
+ callback(refs)
+ selectable: true
+ filterable: true
+ filterByText: true
+ fieldName: 'ref'
+ renderRow: (ref) ->
+ if ref.header?
+ "<li class='dropdown-header'>#{ref.header}</li>"
+ else
+ isActiveClass = if ref is selected then 'is-active' else ''
+
+ "<li>
+ <a href='#' data-ref='#{escape(ref)}' class='#{isActiveClass}'>
+ #{ref}
+ </a>
+ </li>"
+ id: (obj, $el) ->
+ $el.data('ref')
+ toggleLabel: (obj, $el) ->
+ $el.text().trim()
+ clicked: (e) ->
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
index c9cb0f4bb32..12340bbce54 100644
--- a/app/assets/javascripts/right_sidebar.js.coffee
+++ b/app/assets/javascripts/right_sidebar.js.coffee
@@ -43,6 +43,59 @@ class @Sidebar
$('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' })
+ $(document)
+ .off 'click', '.js-issuable-todo'
+ .on 'click', '.js-issuable-todo', @toggleTodo
+
+ toggleTodo: (e) =>
+ $this = $(e.currentTarget)
+ $todoLoading = $('.js-issuable-todo-loading')
+ $btnText = $('.js-issuable-todo-text', $this)
+ ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST'
+
+ if $this.attr('data-delete-path')
+ url = "#{$this.attr('data-delete-path')}"
+ else
+ url = "#{$this.data('url')}"
+
+ $.ajax(
+ url: url
+ type: ajaxType
+ dataType: 'json'
+ data:
+ issuable_id: $this.data('issuable-id')
+ issuable_type: $this.data('issuable-type')
+ beforeSend: =>
+ @beforeTodoSend($this, $todoLoading)
+ ).done (data) =>
+ @todoUpdateDone(data, $this, $btnText, $todoLoading)
+
+ beforeTodoSend: ($btn, $todoLoading) ->
+ $btn.disable()
+ $todoLoading.removeClass 'hidden'
+
+ todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
+ $todoPendingCount = $('.todos-pending-count')
+ $todoPendingCount.text data.count
+
+ $btn.enable()
+ $todoLoading.addClass 'hidden'
+
+ if data.count is 0
+ $todoPendingCount.addClass 'hidden'
+ else
+ $todoPendingCount.removeClass 'hidden'
+
+ if data.delete_path?
+ $btn
+ .attr 'aria-label', $btn.data('mark-text')
+ .attr 'data-delete-path', data.delete_path
+ $btnText.text $btn.data('mark-text')
+ else
+ $btn
+ .attr 'aria-label', $btn.data('todo-text')
+ .removeAttr 'data-delete-path'
+ $btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
@@ -117,5 +170,3 @@ class @Sidebar
getBlock: (name) ->
@sidebar.find(".block.#{name}")
-
-
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/star.js.coffee b/app/assets/javascripts/star.js.coffee
index f27780dda93..01b28171f72 100644
--- a/app/assets/javascripts/star.js.coffee
+++ b/app/assets/javascripts/star.js.coffee
@@ -9,9 +9,11 @@ class @Star
$this.parent().find('.star-count').text data.star_count
if isStarred
$starSpan.removeClass('starred').text 'Star'
+ gl.utils.updateTooltipTitle $this, 'Star project'
$starIcon.removeClass('fa-star').addClass 'fa-star-o'
else
$starSpan.addClass('starred').text 'Unstar'
+ gl.utils.updateTooltipTitle $this, 'Unstar project'
$starIcon.removeClass('fa-star-o').addClass 'fa-star'
return
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 88246b0feb8..2548efb2186 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -31,7 +31,7 @@ class @UsersSelect
assignTo = (selected) ->
data = {}
data[abilityName] = {}
- data[abilityName].assignee_id = selected
+ data[abilityName].assignee_id = if selected? then selected else null
$loading
.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
@@ -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.scss b/app/assets/stylesheets/framework.scss
index 3cbddc59f11..a306b8f3f29 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -37,3 +37,4 @@
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
+@import "framework/blank";
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
new file mode 100644
index 00000000000..40b5171a8c6
--- /dev/null
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -0,0 +1,23 @@
+.blank-state {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+
+.blank-state-no-icon {
+ padding-top: 40px;
+ padding-bottom: 40px;
+}
+
+.blank-state-title {
+ margin-top: 0;
+ margin-bottom: 5px;
+ font-size: 19px;
+ font-weight: normal;
+}
+
+.blank-state-text {
+ margin-top: 0;
+ margin-bottom: $gl-padding;
+ font-size: 15px;
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index fab96404a6c..38023818709 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -91,6 +91,26 @@
background-color: $white-light;
border-top: none;
}
+
+ &.top-block .container-fluid {
+ background-color: inherit;
+ }
+}
+
+.sub-header-block {
+ background-color: $white-light;
+ border-bottom: 1px solid $white-dark;
+ padding: 11px 0;
+ margin-bottom: 11px;
+
+ .oneline {
+ line-height: 35px;
+ }
+
+ &.no-bottom-space {
+ border-bottom: 0;
+ margin-bottom: 0;
+ }
}
.cover-block {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d4d579a083d..00111dfa706 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -461,10 +461,12 @@
}
}
- .ui-state-active,
- .ui-state-hover {
- color: $md-link-color;
- background-color: $calendar-hover-bg;
+ .ui-datepicker-calendar {
+ .ui-state-hover,
+ .ui-state-active {
+ color: #fff;
+ border: 0;
+ }
}
.ui-datepicker-prev,
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/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index fd885b38680..fd8eaa8a691 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -65,6 +65,11 @@
a {
padding-top: 0;
line-height: 1;
+ border-bottom: 1px solid $border-color;
+
+ &.btn.btn-xs {
+ padding: 2px 5px;
+ }
}
}
}
@@ -97,5 +102,30 @@
white-space: pre-wrap;
word-break: keep-all;
}
+
+ @include bulleted-list;
+ }
+}
+
+.toolbar-group {
+ float: left;
+ margin-right: -5px;
+ margin-left: $gl-padding;
+
+ &:first-child {
+ margin-left: 0;
+ }
+}
+
+.toolbar-btn {
+ float: left;
+ padding: 0 5px;
+ color: #959494;
+ background: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 828e7224231..5ec5a96a597 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -110,3 +110,17 @@
font-size: 16px;
line-height: 24px;
}
+
+@mixin bulleted-list {
+ > ul {
+ list-style-type: disc;
+
+ ul {
+ list-style-type: circle;
+
+ ul {
+ list-style-type: square;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index d4e5cc819a4..c74682dfef4 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -52,6 +52,19 @@
.git-clone-holder {
display: none;
}
+
+ // Display Star and Fork buttons without counters on mobile.
+ .project-action-buttons {
+ display: block;
+
+ .count-buttons .btn {
+ margin: 0 10px;
+ }
+
+ .count-buttons .count-with-arrow {
+ display: none;
+ }
+ }
}
.project-stats {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 4de89daeb36..0281b06d3ba 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -18,6 +18,13 @@
opacity: 0;
transition-duration: .3s;
}
+
+ .fa {
+ position: relative;
+ top: 3px;
+ font-size: 13px;
+ color: $btn-placeholder-gray;
+ }
}
@mixin scrolling-links() {
@@ -74,6 +81,7 @@
.container-fluid {
background-color: $background-color;
+ margin-bottom: 0;
}
li {
@@ -103,10 +111,6 @@
width: 50%;
line-height: 28px;
- &.wiki-page {
- padding: 16px 10px 11px;
- }
-
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
width: 100%;
@@ -135,7 +139,7 @@
}
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-max) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
@@ -219,6 +223,7 @@
form {
display: block;
height: auto;
+ margin-bottom: 14px;
input {
width: 100%;
@@ -241,6 +246,12 @@
}
}
}
+
+ &.adjust {
+ .nav-text, .nav-controls {
+ width: auto;
+ }
+ }
}
.layout-nav {
@@ -250,7 +261,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 {
@@ -280,11 +291,10 @@
}
.dropdown {
- margin-left: 7px;
-
- @media (max-width: $screen-xs-min) {
- margin-left: 0;
- }
+ position: absolute;
+ top: 7px;
+ right: 15px;
+ z-index: 2;
li.active {
font-weight: bold;
@@ -313,11 +323,19 @@
.fade-right {
@include fade(left, rgba(250, 250, 250, 0.4), $background-color);
right: 0;
+
+ .fa {
+ right: -7px;
+ }
}
.fade-left {
@include fade(right, rgba(250, 250, 250, 0.4), $background-color);
left: 0;
+
+ .fa {
+ left: -7px;
+ }
}
li {
@@ -347,6 +365,12 @@
.badge {
color: $gl-icon-color;
}
+
+ &:hover {
+ a, i {
+ color: $black;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index ae7bdf14c40..874416e1007 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -9,6 +9,10 @@
margin-top: -2px;
float: right;
}
+
+ .dropdown-menu-toggle {
+ line-height: 20px;
+ }
}
.panel-body {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index f242706ebe4..21d87cc9d34 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -165,11 +165,6 @@
background-size: 16px 16px !important;
}
-/** Branch/tag selector **/
-.project-refs-form .select2-container {
- width: 160px !important;
-}
-
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index b7ec3f70bfb..98f917ce69b 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,58 +39,52 @@
}
}
-.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;
}
+ .icon-container {
+ width: 34px;
+ display: inline-block;
+ text-align: center;
+ }
+
a {
- width: $sidebar_width;
- padding: 7px 15px 7px 23px;
+ padding: 7px 15px 7px 12px;
font-size: $gl-font-size;
line-height: 24px;
display: block;
@@ -93,11 +92,9 @@
font-weight: normal;
outline: none;
- &:hover {
- text-decoration: none;
- }
-
- &:active, &:focus {
+ &:hover,
+ &:active,
+ &:focus {
text-decoration: none;
}
@@ -109,10 +106,6 @@
svg {
margin-right: 13px;
}
-
- &.back-link i {
- transition-duration: .3s;
- }
}
}
@@ -123,37 +116,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);
+ }
}
}
@@ -162,62 +168,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/commit.scss b/app/assets/stylesheets/pages/commit.scss
index fc3f214aba5..35ab28b3fea 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -26,6 +26,8 @@
.commit-info-row {
margin-bottom: 10px;
+ line-height: 24px;
+ padding-top: 6px;
&.commit-info-row-header {
line-height: 34px;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c8c6bbde084..de534d28421 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -7,84 +7,119 @@
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;
+ position: relative;
- .str-truncated {
- max-width: 70%;
+ @media (min-width: $screen-sm-min) {
+ padding-left: 20px;
+
+ .commit-info-block {
+ padding-left: 44px;
}
+ }
- .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 {
+ position: absolute;
+ top: 10px;
+ left: 16px;
}
.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 +128,7 @@ li.commit {
background: inherit;
padding: 0;
margin: 0;
+ white-space: pre-wrap;
}
a {
@@ -102,7 +138,7 @@ li.commit {
.commit-row-info {
color: $gl-gray;
- line-height: 24px;
+ line-height: 1;
a {
color: $gl-gray;
@@ -111,10 +147,6 @@ li.commit {
.avatar {
margin-right: 8px;
}
-
- .committed_ago {
- display: inline-block;
- }
}
&.inline-commit {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 1a7d5f9666e..5286b73cc50 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -4,6 +4,11 @@
margin-bottom: $gl-padding;
border-radius: 3px;
+ .commit-short-id {
+ font-family: $regular_font;
+ font-weight: 400;
+ }
+
.diff-header {
position: relative;
background: $background-color;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 22679c764dc..1aa4e06d975 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -60,14 +60,14 @@
.encoding-selector,
.license-selector,
- .gitignore-selector {
+ .gitignore-selector,
+ .gitlab-ci-yml-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
}
- .gitignore-selector {
-
+ .gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown {
line-height: 21px;
}
@@ -77,4 +77,10 @@
width: 220px;
}
}
+
+ .gitlab-ci-yml-selector {
+ .dropdown-menu-toggle {
+ width: 250px;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
new file mode 100644
index 00000000000..e160d676e35
--- /dev/null
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -0,0 +1,5 @@
+.environments {
+ .commit-title {
+ margin: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 6fe57c737b3..a2145956eb5 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -54,6 +54,10 @@
}
}
+ code {
+ white-space: pre-wrap;
+ }
+
pre {
border: none;
background: #f9f9f9;
@@ -136,9 +140,10 @@
.event-last-push {
overflow: auto;
width: 100%;
+
.event-last-push-text {
@include str-truncated(100%);
- padding: 5px 0;
+ padding: 4px 0;
font-size: 13px;
float: left;
margin-right: -150px;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index ec6c099df5b..ac7721cbe15 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -39,3 +39,20 @@
}
}
}
+
+.groups-cover-block {
+
+ .container-fluid {
+ position: relative;
+ }
+
+ .access-request-button {
+ @include btn-gray;
+ position: absolute;
+ right: 16px;
+ bottom: 32px;
+ padding: 3px 10px;
+ text-transform: none;
+ background-color: $background-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 4a95b7b852e..0b710ef168b 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -57,4 +57,11 @@
.documentation {
padding: 7px;
+
+ // Border around images in the help pages.
+ img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px;
+ }
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ea453ce356a..21ff6ab71f0 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -4,6 +4,13 @@
margin-right: 1px;
}
}
+
+ // Border around images in issue and MR descriptions.
+ .description img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px;
+ }
}
.issuable-filter-count {
@@ -34,6 +41,10 @@
color: inherit;
}
+ .issuable-header-text {
+ margin-top: 7px;
+ }
+
.block {
@include clearfix;
padding: $gl-padding 0;
@@ -60,10 +71,6 @@
margin-top: 0;
}
- .issuable-count {
- margin-top: 7px;
- }
-
.gutter-toggle {
margin-left: 20px;
padding-left: 10px;
@@ -145,7 +152,6 @@
.assign-yourself {
margin-top: 10px;
- font-weight: normal;
display: block;
}
}
@@ -158,6 +164,10 @@
font-weight: normal;
}
+ .no-value {
+ color: $gl-placeholder-color;
+ }
+
.sidebar-collapsed-icon {
display: none;
}
@@ -248,11 +258,16 @@
padding-bottom: 0;
margin-bottom: 10px;
}
+
+ .issuable-header-btn {
+ display: none;
+ }
}
- .issuable-pager {
+ .issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
+
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
@@ -263,7 +278,7 @@
}
}
- a:not(.issuable-pager) {
+ a {
&:hover {
color: $md-link-color;
text-decoration: none;
@@ -322,7 +337,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..f5f67e2cd84 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -50,11 +50,10 @@
.label-row {
.label-name {
- display: block;
+ display: inline-block;
margin-bottom: 10px;
@media (min-width: $screen-sm-min) {
- display: inline-block;
width: 200px;
margin-bottom: 0;
}
@@ -63,6 +62,7 @@
.label-description {
display: block;
margin-bottom: 10px;
+ margin-left: 50px;
@media (min-width: $screen-sm-min) {
display: inline-block;
@@ -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 a47f2580aa3..aca82f7f7bf 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -119,7 +119,12 @@
margin-bottom: 0;
}
- @media (max-width: $screen-sm-max) {
+ .btn-grouped {
+ margin-left: 0;
+ margin-right: 7px;
+ }
+
+ @media (max-width: $screen-xs-max) {
h4 {
font-size: 15px;
}
@@ -131,10 +136,14 @@
.btn,
.btn-group,
.accept-action {
- width: 100%;
margin-bottom: 4px;
}
+ .accept-action {
+ width: 100%;
+ text-align: center;
+ }
+
.accept-control {
width: 100%;
text-align: center;
@@ -244,6 +253,10 @@
.panel-footer {
padding: 5px 10px;
+
+ .btn {
+ min-width: auto;
+ }
}
.commit {
@@ -252,9 +265,7 @@
}
.avatar {
- width: 20px;
- height: 20px;
- margin-right: 5px;
+ margin-left: 0;
}
.commit-row-info {
@@ -282,7 +293,7 @@
margin-bottom: 0;
}
- @media (min-width: $screen-sm-min) {
+ @media (min-width: $screen-xs-min) {
float: left;
width: 50%;
margin-bottom: 0;
@@ -313,3 +324,13 @@
}
}
}
+
+.merged-buttons {
+ .btn {
+ float: left;
+
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 577dddae741..3784010348a 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -179,6 +179,10 @@
border-top: 1px solid $border-color;
}
+.md-helper {
+ padding-top: 10px;
+}
+
.toolbar-button {
padding: 0;
background: none;
@@ -219,3 +223,16 @@
float: left;
}
}
+
+.note-form-actions {
+ @media (max-width: $screen-xs-max) {
+ .btn {
+ float: none;
+ width: 100%;
+
+ &:not(:last-child) {
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 0c084118753..ffba3dc5bc6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -84,24 +84,14 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
+ // Reset ul style types since we're nested inside a ul already
+ @include bulleted-list;
+
// On diffs code should wrap nicely and not overflow
code {
white-space: pre-wrap;
}
- // Reset ul style types since we're nested inside a ul already
- & > ul {
- list-style-type: disc;
-
- ul {
- list-style-type: circle;
-
- ul {
- list-style-type: square;
- }
- }
- }
-
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
@@ -117,6 +107,13 @@ ul.notes {
code {
word-break: keep-all;
}
+
+ // Border around images in issue and MR comments.
+ img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px 0;
+ }
}
}
@@ -139,6 +136,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 +261,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 ea708872cc8..d3e59d7fdb9 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;
@@ -106,7 +101,8 @@
.notifications-btn {
- .fa-bell {
+ .fa-bell,
+ .fa-spinner {
margin-right: 6px;
}
@@ -224,13 +220,20 @@
right: 16px;
bottom: 0;
- .btn {
- padding: 3px 10px;
- background-color: $background-color;
+ @media (max-width: $screen-md-max) {
+ top: 0;
}
- @media (max-width: 1304px) {
- top: 0;
+ .access-request-button {
+ position: absolute;
+ right: 0;
+ bottom: 61px;
+
+ @media (max-width: $screen-md-max) {
+ position: relative;
+ bottom: 0;
+ margin-right: 10px;
+ }
}
}
@@ -281,10 +284,6 @@
color: #555;
}
-.project_member_row form {
- margin: 0;
-}
-
.transfer-project .select2-container {
min-width: 200px;
}
@@ -368,13 +367,14 @@ a.deploy-project-label {
.project-import .btn {
float: left;
+ margin-bottom: 10px;
margin-right: 10px;
}
.project-stats {
margin-top: $gl-padding;
margin-bottom: 0;
- padding: 16px 0;
+ padding: 0;
background-color: $white-light;
font-size: 0;
@@ -383,13 +383,14 @@ a.deploy-project-label {
}
.nav li {
- display: inline;
+ display: inline-block;
+ margin: 16px 0;
+ margin-right: 16px;
}
.nav > li > a {
background-color: transparent;
- margin-right: 12px;
- padding: 0 10px;
+ padding: 5px 10px;
font-size: 15px;
color: $notes-light-color;
}
@@ -403,12 +404,17 @@ a.deploy-project-label {
font-size: 17px;
}
- li.missing a {
- color: #5a6069;
- border: 1px dashed #dce0e5;
+ li.missing {
+ border: 1px dashed $border-gray-light;
+ border-radius: $border-radius-default;
+
+ a {
+ color: $notes-light-color;
+ display: block;
+ }
&:hover {
- background-color: #f0f2f5;
+ background-color: $gray-normal;
}
}
@@ -495,7 +501,8 @@ pre.light-well {
.activity-filter-block {
.controls {
- padding-bottom: 10px;
+ padding-bottom: 7px;
+ margin-top: 8px;
border-bottom: 1px solid $border-color;
}
}
@@ -616,3 +623,9 @@ pre.light-well {
color: $gl-text-green;
}
}
+
+.project-refs-form {
+ .dropdown-menu {
+ width: 300px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 85a0304196c..69288b31cc4 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -14,24 +14,38 @@
font-size: 10px;
}
+#contributors-master {
+ @include make-md-column(12);
+
+ svg {
+ width: 100%;
+ }
+}
+
#contributors {
.contributors-list {
margin: 0 0 10px;
list-style: none;
padding: 0;
+
+ svg {
+ width: 100%;
+ }
}
.person {
- &:nth-child(even) {
- float: right;
- }
- float: left;
+ @include make-md-column(6);
margin-top: 10px;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
}
.person .spark {
display: block;
background: #f3f3f3;
+ width: 100%;
}
.person .area-contributor {
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index afc00a68572..cf16d070cfe 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -62,6 +62,10 @@
}
}
+ code {
+ white-space: pre-wrap;
+ }
+
pre {
border: none;
background: #f9f9f9;
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/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 26cf74e4849..4b0ec54b3f4 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -5,6 +5,7 @@ class Admin::AppearancesController < Admin::ApplicationController
end
def preview
+ render 'preview', layout: 'devise'
end
def create
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index d25619d94e0..bf20c5305a7 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -1,15 +1,14 @@
class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create]
- def index
- @runner_projects = project.runner_projects.all
- @runner_project = project.runner_projects.new
- end
-
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
- if @runner.assign_to(@project, current_user)
+ return head(403) if @runner.is_shared? || @runner.locked?
+
+ runner_project = @runner.assign_to(@project, current_user)
+
+ if runner_project.persisted?
redirect_to admin_runner_path(@runner)
else
redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index cd6ae507cf1..9cc31620d9f 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)
@@ -36,6 +36,10 @@ class ApplicationController < ActionController::Base
render_404
end
+ rescue_from Gitlab::Access::AccessDeniedError do |exception|
+ render_403
+ end
+
def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options
end
@@ -64,17 +68,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 +323,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/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 3865b2d61fd..c89678cf2d8 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -35,6 +35,7 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id])
projects = current_user.authorized_projects
+ projects = projects.search(params[:search]) if params[:search].present?
projects = projects.select do |project|
current_user.can?(:admin_issue, project)
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
new file mode 100644
index 00000000000..52dc396af6a
--- /dev/null
+++ b/app/controllers/concerns/membership_actions.rb
@@ -0,0 +1,43 @@
+module MembershipActions
+ extend ActiveSupport::Concern
+ include MembersHelper
+
+ def request_access
+ membershipable.request_access(current_user)
+
+ redirect_to polymorphic_path(membershipable),
+ notice: 'Your request for access has been queued for review.'
+ end
+
+ def approve_access_request
+ @member = membershipable.members.request.find(params[:id])
+
+ return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
+
+ @member.accept_request
+
+ redirect_to polymorphic_url([membershipable, :members])
+ end
+
+ def leave
+ @member = membershipable.members.find_by(user_id: current_user)
+ Members::DestroyService.new(@member, current_user).execute
+
+ source_type = @member.real_source_type.humanize(capitalize: false)
+ notice =
+ if @member.request?
+ "Your access request to the #{source_type} has been withdrawn."
+ else
+ "You left the \"#{@member.source.human_name}\" #{source_type}."
+ end
+ redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
+
+ redirect_to redirect_path, notice: notice
+ end
+
+ protected
+
+ def membershipable
+ raise NotImplementedError
+ end
+end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index f9a1929c117..3a2db3e6eeb 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,44 +1,39 @@
class Dashboard::TodosController < Dashboard::ApplicationController
- before_action :find_todos, only: [:index, :destroy, :destroy_all]
+ include TodosHelper
+
+ before_action :find_todos, only: [:index, :destroy_all]
def index
@todos = @todos.page(params[:page])
end
def destroy
- todo.done
-
- todo_notice = 'Todo was successfully marked as done.'
+ TodoService.new.mark_todos_as_done([todo], current_user)
respond_to do |format|
- format.html { redirect_to dashboard_todos_path, notice: todo_notice }
+ format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.js { head :ok }
- format.json do
- render json: { count: @todos.size, done_count: current_user.todos.done.count }
- end
+ format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
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 }
- end
+ format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
end
end
private
def todo
- @todo ||= current_user.todos.find(params[:id])
+ @todo ||= find_todos.find(params[:id])
end
def find_todos
- @todos = TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, params).execute
end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 48dbf656e84..2c49fe3833e 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,11 +1,13 @@
class Groups::GroupMembersController < Groups::ApplicationController
+ include MembershipActions
+
# Authorize
- before_action :authorize_admin_group_member!, except: [:index, :leave]
+ before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
- @members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -34,9 +36,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
def destroy
@group_member = @group.group_members.find(params[:id])
- return render_403 unless can?(current_user, :destroy_group_member, @group_member)
-
- @group_member.destroy
+ Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
@@ -58,25 +58,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
- def leave
- @group_member = @group.group_members.find_by(user_id: current_user)
-
- if can?(current_user, :destroy_group_member, @group_member)
- @group_member.destroy
-
- redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
- else
- if @group.last_owner?(current_user)
- redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
- else
- return render_403
- end
- end
- end
-
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
+
+ # MembershipActions concern
+ alias_method :membershipable, :group
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/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 735e562a497..aacbefd4ab8 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -39,7 +39,7 @@ class NotificationSettingsController < ApplicationController
def render_response
render json: {
- html: view_to_html_string("notifications/buttons/_notifications", notification_setting: @notification_setting),
+ html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
saved: @saved
}
end
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 175afbf8425..69959fe3687 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -5,7 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController
def unlink
provider = params[:provider]
- current_user.identities.find_by(provider: provider).destroy
+ current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
redirect_to profile_account_path
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/application_controller.rb b/app/controllers/projects/application_controller.rb
index 776ba92c9ab..996909a28c6 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -74,7 +74,7 @@ class Projects::ApplicationController < ApplicationController
end
def require_branch_head
- unless @repository.branch_names.include?(@ref)
+ unless @repository.branch_exists?(@ref)
redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref),
notice: "This action is not allowed unless you are on a branch"
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 832d7deb57d..f11c8321464 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,22 +1,18 @@
class Projects::ArtifactsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_build!
+ before_action :authorize_update_build!, only: [:keep]
+ before_action :validate_artifacts!
def download
unless artifacts_file.file_storage?
return redirect_to artifacts_file.url
end
- unless artifacts_file.exists?
- return render_404
- end
-
send_file artifacts_file.path, disposition: 'attachment'
end
def browse
- return render_404 unless build.artifacts?
-
directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory)
@@ -34,8 +30,17 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
end
+ def keep
+ build.keep_artifacts!
+ redirect_to namespace_project_build_path(project.namespace, project, build)
+ end
+
private
+ def validate_artifacts!
+ render_404 unless build.artifacts?
+ end
+
def build
@build ||= project.builds.find_by!(id: params[:build_id])
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 14c82826342..ef3051d7519 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -51,7 +51,7 @@ class Projects::BuildsController < Projects::ApplicationController
return render_404
end
- build = Ci::Build.retry(@build)
+ build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 20637fa46fe..6751737d15e 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -46,7 +46,7 @@ class Projects::CommitController < Projects::ApplicationController
def retry_builds
ci_builds.latest.failed.each do |build|
if build.retryable?
- Ci::Build.retry(build)
+ Ci::Build.retry(build, current_user)
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
new file mode 100644
index 00000000000..4b433796161
--- /dev/null
+++ b/app/controllers/projects/environments_controller.rb
@@ -0,0 +1,49 @@
+class Projects::EnvironmentsController < Projects::ApplicationController
+ layout 'project'
+ before_action :authorize_read_environment!
+ before_action :authorize_create_environment!, only: [:new, :create]
+ before_action :authorize_update_environment!, only: [:destroy]
+ before_action :environment, only: [:show, :destroy]
+
+ def index
+ @environments = project.environments
+ end
+
+ def show
+ @deployments = environment.deployments.order(id: :desc).page(params[:page])
+ end
+
+ def new
+ @environment = project.environments.new
+ end
+
+ def create
+ @environment = project.environments.create(create_params)
+
+ if @environment.persisted?
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ else
+ render 'new'
+ end
+ end
+
+ def destroy
+ if @environment.destroy
+ flash[:notice] = 'Environment was successfully removed.'
+ else
+ flash[:alert] = 'Failed to remove environment.'
+ end
+
+ redirect_to namespace_project_environments_path(project.namespace, project)
+ end
+
+ private
+
+ def create_params
+ params.require(:environment).permit(:name)
+ end
+
+ def environment
+ @environment ||= project.environments.find(params[:id])
+ 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/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index cac440ae53e..487963fdcd7 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -32,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def retry
- pipeline.retry_failed
+ pipeline.retry_failed(current_user)
redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
end
@@ -54,6 +54,6 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def commit
- @commit ||= @pipeline.commit_data
+ @commit ||= @pipeline.commit
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index cdea5f0b776..6ba32d33403 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,10 +1,12 @@
class Projects::ProjectMembersController < Projects::ApplicationController
+ include MembershipActions
+
# Authorize
- before_action :authorize_admin_project_member!, except: [:leave, :index]
+ before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@project_members = @project.project_members
- @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
+ @project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
@@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC')
@group = @project.group
+
if @group
@group_members = @group.group_members
- @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
+ @group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -47,9 +50,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def destroy
@project_member = @project.project_members.find(params[:id])
- return render_403 unless can?(current_user, :destroy_project_member, @project_member)
-
- @project_member.destroy
+ Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format|
format.html do
@@ -73,26 +74,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
- def leave
- @project_member = @project.project_members.find_by(user_id: current_user)
-
- if can?(current_user, :destroy_project_member, @project_member)
- @project_member.destroy
-
- respond_to do |format|
- format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
- format.js { head :ok }
- end
- else
- if current_user == @project.owner
- message = 'You can not leave your own project. Transfer or delete the project.'
- redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
- else
- render_403
- end
- end
- end
-
def apply_import
source_project = Project.find(params[:source_project_id])
@@ -112,4 +93,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
+
+ # MembershipActions concern
+ alias_method :membershipable, :project
end
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index bedeb4a295c..dc1a18f8d42 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -6,11 +6,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
+ return head(403) if @runner.is_shared? || @runner.locked?
return head(403) unless current_user.ci_authorized_runners.include?(@runner)
path = runners_path(project)
+ runner_project = @runner.assign_to(project, current_user)
- if @runner.assign_to(project, current_user)
+ if runner_project.persisted?
redirect_to path
else
redirect_to path, alert: 'Failed adding runner to project'
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 0b4fa572501..53c36635efe 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @runners = project.runners.ordered
- @specific_runners = current_user.ci_authorized_runners.
- where.not(id: project.runners).
- ordered.page(params[:page]).per(20)
+ @project_runners = project.runners.ordered
+ @assignable_runners = current_user.ci_authorized_runners.
+ assignable_for(project).ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
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
new file mode 100644
index 00000000000..23868d986e9
--- /dev/null
+++ b/app/controllers/projects/todos_controller.rb
@@ -0,0 +1,31 @@
+class Projects::TodosController < Projects::ApplicationController
+ before_action :authenticate_user!, only: [:create]
+
+ def create
+ todo = TodoService.new.mark_todo(issuable, current_user)
+
+ render json: {
+ count: current_user.todos_pending_count,
+ delete_path: dashboard_todo_path(todo)
+ }
+ end
+
+ private
+
+ def issuable
+ @issuable ||= begin
+ case params[:issuable_type]
+ when "issue"
+ issue = @project.issues.find(params[:issuable_id])
+
+ if can?(current_user, :read_issue, issue)
+ issue
+ else
+ render_404
+ end
+ when "merge_request"
+ @project.merge_requests.find(params[:issuable_id])
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 2aa6bed0724..7ec1e73b3be 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController
if @page
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
+ response.headers['Content-Security-Policy'] = "default-src 'none'"
+ response.headers['X-Content-Security-Policy'] = "default-src 'none'"
+
if file.on_disk?
send_file file.on_disk_path, disposition: 'inline'
else
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a6479c42d94..2b1f50fd01e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,13 +1,13 @@
class ProjectsController < Projects::ApplicationController
include ExtractsPath
- before_action :authenticate_user!, except: [:show, :activity]
+ before_action :authenticate_user!, except: [:show, :activity, :refs]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
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
@@ -208,6 +251,24 @@ class ProjectsController < Projects::ApplicationController
}
end
+ def refs
+ options = {
+ 'Branches' => @repository.branch_names,
+ }
+
+ unless @repository.tag_count.zero?
+ options['Tags'] = VersionSorter.rsort(@repository.tag_names)
+ end
+
+ # If reference is commit id - we should add it to branch/tag selectbox
+ ref = Addressable::URI.unescape(params[:ref])
+ if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
+ options['Commits'] = [ref]
+ end
+
+ render json: options.to_json
+ end
+
private
def determine_layout
@@ -242,8 +303,14 @@ class ProjectsController < Projects::ApplicationController
project.repository_exists? && !project.empty_repo?
end
- # Override get_id from ExtractsPath, which returns the branch and file path
+ # Override extract_ref from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch.
+ # This way we avoid to access the repository.ref_names.
+ def extract_ref(_id)
+ [get_id, '']
+ end
+
+ # Override get_id from ExtractsPath in this case is just the root of the default branch.
def get_id
project.repository.root_ref
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index dae8f7b1447..17aed816cbd 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -40,7 +40,7 @@ class SessionsController < Devise::SessionsController
# Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change.
def check_initial_setup
- return unless User.count == 1
+ return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one
user = User.admins.last
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index ee14ac60fb4..0b7832e6583 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -12,7 +12,7 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).non_diff_notes
when "issue"
- project.issues.find(target_id).notes.inc_author
+ project.issues.visible_to_user(current_user).find(target_id).notes.inc_author
when "merge_request"
project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
when "snippet", "project_snippet"
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 01cbf91c658..00ff1611039 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -51,7 +51,7 @@ class SnippetsFinder
snippets = project.snippets.fresh
if current_user
- if project.team.member?(current_user.id) || current_user.admin?
+ if project.team.member?(current_user) || current_user.admin?
snippets
else
snippets.public_and_internal
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 1d88116d7d2..58a00f88af7 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -36,7 +36,7 @@ class TodosFinder
private
def action_id?
- action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
+ action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
end
def action_id
@@ -123,7 +123,7 @@ class TodosFinder
end
def by_state(items)
- case params[:state]
+ case params[:state].to_s
when 'done'
items.done
else
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 439b015b3b8..41859841834 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -101,22 +101,6 @@ module ApplicationHelper
'Never'
end
- def grouped_options_refs
- repository = @project.repository
-
- options = [
- ['Branches', repository.branch_names],
- ['Tags', VersionSorter.rsort(repository.tag_names)]
- ]
-
- # If reference is commit id - we should add it to branch/tag selectbox
- if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/
- options << ['Commit', [@ref]]
- end
-
- grouped_options_for_select(options, @ref || @project.default_branch)
- end
-
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
@@ -132,7 +116,7 @@ module ApplicationHelper
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
# Skip if user removed branch right after that
- return false unless project.repository.branch_names.include?(event.branch_name)
+ return false unless project.repository.branch_exists?(event.branch_name)
true
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index cec2dc753fe..4b4bc3d4276 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -29,7 +29,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
- link_to "Edit", edit_path, class: 'btn btn-file-option'
+ link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
@@ -116,7 +116,7 @@ module BlobHelper
end
def blob_text_viewable?(blob)
- blob && blob.text? && !blob.lfs_pointer?
+ blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw?
end
def blob_size(blob)
@@ -180,18 +180,22 @@ 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
def gitignore_names
- return @gitignore_names if defined?(@gitignore_names)
+ @gitignore_names ||=
+ Gitlab::Template::Gitignore.categories.keys.map do |k|
+ [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
+ end.to_h
+ end
- @gitignore_names = {
- Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } },
- # Note that the key here doesn't cover it really
- Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } }
- }
+ def gitlab_ci_ymls
+ @gitlab_ci_ymls ||=
+ Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
+ [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
+ end.to_h
end
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 3ee3fc74f0c..c533659b600 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -10,7 +10,7 @@ module BranchesHelper
end
def can_push_branch?(project, branch_name)
- return false unless project.repository.branch_names.include?(branch_name)
+ return false unless project.repository.branch_exists?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
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 d328f56c80c..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
@@ -129,7 +139,7 @@ module CommitsHelper
tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip
if can_collaborate_with_project?
- btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil?
+ btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil?
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
@@ -141,7 +151,7 @@ module CommitsHelper
namespace_key: current_user.namespace.id,
continue: continue_params)
- btn_class = "btn btn-grouped btn-close" unless btn_class.nil?
+ btn_class = "btn btn-grouped btn-warning" unless btn_class.nil?
link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
@@ -153,7 +163,7 @@ module CommitsHelper
tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project?
- btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil?
+ btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil?
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
@@ -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/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 067a00660aa..1a259656f31 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -50,8 +50,6 @@ module GitlabMarkdownHelper
context[:project] ||= @project
- text = Banzai.pre_process(text, context)
-
html = Banzai.render(text, context)
context.merge!(
@@ -185,4 +183,17 @@ module GitlabMarkdownHelper
''
end
end
+
+ def markdown_toolbar_button(options = {})
+ data = options[:data].merge({ container: "body" })
+ content_tag :button,
+ type: "button",
+ class: "toolbar-btn js-md has-tooltip hidden-xs",
+ tabindex: -1,
+ data: data,
+ title: options[:title],
+ aria: { label: options[:title] } do
+ icon(options[:icon])
+ end
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 2ce2d4e694f..5386ddadc62 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -13,10 +13,23 @@
# merge_request_path(merge_request)
#
module GitlabRoutingHelper
+ # Project
def project_path(project, *args)
namespace_project_path(project.namespace, project, *args)
end
+ def project_url(project, *args)
+ namespace_project_url(project.namespace, project, *args)
+ end
+
+ def edit_project_path(project, *args)
+ edit_namespace_project_path(project.namespace, project, *args)
+ end
+
+ def edit_project_url(project, *args)
+ edit_namespace_project_url(project.namespace, project, *args)
+ end
+
def project_files_path(project, *args)
namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
end
@@ -29,6 +42,10 @@ module GitlabRoutingHelper
namespace_project_pipelines_path(project.namespace, project, *args)
end
+ def project_environments_path(project, *args)
+ namespace_project_environments_path(project.namespace, project, *args)
+ end
+
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
@@ -41,10 +58,6 @@ module GitlabRoutingHelper
activity_namespace_project_path(project.namespace, project, *args)
end
- def edit_project_path(project, *args)
- edit_namespace_project_path(project.namespace, project, *args)
- end
-
def runners_path(project, *args)
namespace_project_runners_path(project.namespace, project, *args)
end
@@ -65,14 +78,6 @@ module GitlabRoutingHelper
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
- def project_url(project, *args)
- namespace_project_url(project.namespace, project, *args)
- end
-
- def edit_project_url(project, *args)
- edit_namespace_project_url(project.namespace, project, *args)
- end
-
def issue_url(entity, *args)
namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args)
end
@@ -92,4 +97,56 @@ module GitlabRoutingHelper
toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity)
end
end
+
+ ## Members
+ def project_members_url(project, *args)
+ namespace_project_project_members_url(project.namespace, project)
+ end
+
+ def project_member_path(project_member, *args)
+ namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+ end
+
+ def request_access_project_members_path(project, *args)
+ request_access_namespace_project_project_members_path(project.namespace, project)
+ end
+
+ def leave_project_members_path(project, *args)
+ leave_namespace_project_project_members_path(project.namespace, project)
+ end
+
+ def approve_access_request_project_member_path(project_member, *args)
+ approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+ end
+
+ def resend_invite_project_member_path(project_member, *args)
+ resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
+ end
+
+ # Groups
+
+ ## Members
+ def group_members_url(group, *args)
+ group_group_members_url(group, *args)
+ end
+
+ def group_member_path(group_member, *args)
+ group_group_member_path(group_member.source, group_member)
+ end
+
+ def request_access_group_members_path(group, *args)
+ request_access_group_group_members_path(group)
+ end
+
+ def leave_group_members_path(group, *args)
+ leave_group_group_members_path(group)
+ end
+
+ def approve_access_request_group_member_path(group_member, *args)
+ approve_access_request_group_group_member_path(group_member.source, group_member)
+ end
+
+ def resend_invite_group_member_path(group_member, *args)
+ resend_invite_group_group_member_path(group_member.source, group_member)
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 4cac69c6795..b9211e88473 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,24 +1,4 @@
module GroupsHelper
- def remove_user_from_group_message(group, member)
- if member.user
- "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
- else
- "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
- end
- end
-
- def leave_group_message(group)
- "Are you sure you want to leave \"#{group}\" group?"
- end
-
- def should_user_see_group_roles?(user, group)
- if user
- user.is_admin? || group.members.exists?(user_id: user.id)
- else
- false
- end
- end
-
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 40d8ce8a1d3..8231ce49fac 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -67,6 +67,12 @@ module IssuablesHelper
end
end
+ def issuable_todo(issuable)
+ if current_user
+ current_user.todos.find_by(target: issuable, state: :pending)
+ end
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
new file mode 100644
index 00000000000..ec106418f2d
--- /dev/null
+++ b/app/helpers/members_helper.rb
@@ -0,0 +1,45 @@
+module MembersHelper
+ # Returns a `<action>_<source>_member` association, e.g.:
+ # - admin_project_member, update_project_member, destroy_project_member
+ # - admin_group_member, update_group_member, destroy_group_member
+ def action_member_permission(action, member)
+ "#{action}_#{member.type.underscore}".to_sym
+ end
+
+ def default_show_roles(member)
+ can?(current_user, action_member_permission(:update, member), member) ||
+ can?(current_user, action_member_permission(:destroy, member), member) ||
+ can?(current_user, action_member_permission(:admin, member), member.source)
+ end
+
+ def remove_member_message(member, user: nil)
+ user = current_user if defined?(current_user)
+
+ text = 'Are you sure you want to '
+ action =
+ if member.request?
+ if member.user == user
+ 'withdraw your access request for'
+ else
+ "deny #{member.user.name}'s request to join"
+ end
+ elsif member.invite?
+ "revoke the invitation for #{member.invite_email} to join"
+ else
+ "remove #{member.user.name} from"
+ end
+
+ text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?"
+ end
+
+ def remove_member_title(member)
+ text = " from #{member.real_source_type.humanize(capitalize: false)}"
+
+ text.prepend(member.request? ? 'Deny access request' : 'Remove user')
+ end
+
+ def leave_confirmation_message(member_source)
+ "Are you sure you want to leave the " \
+ "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
+ end
+end
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/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 8790a66ba14..77783cd7640 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -34,7 +34,7 @@ module NotificationsHelper
def notification_description(level)
case level.to_sym
when :participating
- 'You will only receive notifications from related resources'
+ 'You will only receive notifications for threads you have participated in'
when :mention
'You will receive notifications only for comments in which you were @mentioned'
when :watch
@@ -43,6 +43,8 @@ module NotificationsHelper
'You will not get any notifications via email'
when :global
'Use your global notification setting'
+ when :custom
+ 'You will only receive notifications for the events you choose'
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 5e5d170a9f3..d91e3332e48 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,12 +1,4 @@
module ProjectsHelper
- def remove_from_project_team_message(project, member)
- if member.user
- "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
- else
- "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
- end
- end
-
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
@@ -49,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
@@ -115,14 +107,6 @@ module ProjectsHelper
end
end
- def user_max_access_in_project(user_id, project)
- level = project.team.max_member_access(user_id)
-
- if level
- Gitlab::Access.options_with_owner.key(level)
- end
- end
-
def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil?
@@ -156,6 +140,10 @@ module ProjectsHelper
nav_tabs << :container_registry
end
+ if can?(current_user, :read_environment, project)
+ nav_tabs << :environments
+ end
+
if can?(current_user, :admin_project, project)
nav_tabs << :settings
end
@@ -286,10 +274,6 @@ module ProjectsHelper
end
end
- def leave_project_message(project)
- "Are you sure you want to leave \"#{project.name}\" project?"
- end
-
def new_readme_path
ref = @repository.root_ref if @repository
ref ||= 'master'
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 8142f733e76..b04b0a5114c 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -20,7 +20,6 @@ module TimeHelper
end
end
-
def date_from_to(from, to)
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index b4923fbb138..a832a6c8df7 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
+ TodosFinder.new(current_user, state: :pending).execute.count
end
def todos_done_count
- current_user.todos.done.count
+ TodosFinder.new(current_user, state: :done).execute.count
end
def todo_action_name(todo)
@@ -12,6 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
+ when Todo::MARKED then 'added a todo for'
end
end
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
deleted file mode 100644
index 1c43f95dc8c..00000000000
--- a/app/mailers/emails/groups.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-module Emails
- module Groups
- def group_access_granted_email(group_member_id)
- @group_member = GroupMember.find(group_member_id)
- @group = @group_member.group
-
- @target_url = group_url(@group)
- @current_user = @group_member.user
-
- mail(to: @group_member.user.notification_email,
- subject: subject("Access to group was granted"))
- end
-
- def group_member_invited_email(group_member_id, token)
- @group_member = GroupMember.find group_member_id
- @group = @group_member.group
- @token = token
-
- @target_url = group_url(@group)
- @current_user = @group_member.user
-
- mail(to: @group_member.invite_email,
- subject: "Invitation to join group #{@group.name}")
- end
-
- def group_invite_accepted_email(group_member_id)
- @group_member = GroupMember.find group_member_id
- return if @group_member.created_by.nil?
-
- @group = @group_member.group
-
- @target_url = group_url(@group)
- @current_user = @group_member.created_by
-
- mail(to: @group_member.created_by.notification_email,
- subject: subject("Invitation accepted"))
- end
-
- def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
- return if created_by_id.nil?
-
- @group = Group.find(group_id)
- @current_user = @created_by = User.find(created_by_id)
- @access_level = access_level
- @invite_email = invite_email
-
- @target_url = group_url(@group)
- mail(to: @created_by.notification_email,
- subject: subject("Invitation declined"))
- end
- end
-end
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
new file mode 100644
index 00000000000..45311690293
--- /dev/null
+++ b/app/mailers/emails/members.rb
@@ -0,0 +1,86 @@
+module Emails
+ module Members
+ extend ActiveSupport::Concern
+ include MembersHelper
+
+ included do
+ helper_method :member_source, :member
+ end
+
+ def member_access_requested_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+
+ admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ # A project in a group can have no explicit owners/masters, in that case
+ # we fallbacks to the group's owners/masters.
+ if admins.empty? && member_source.respond_to?(:group) && member_source.group
+ admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ end
+
+ mail(to: admins,
+ subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
+ end
+
+ def member_access_granted_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+
+ mail(to: member.user.notification_email,
+ subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
+ end
+
+ def member_access_denied_email(member_source_type, source_id, user_id)
+ @member_source_type = member_source_type
+ @member_source = member_source_class.find(source_id)
+ requester = User.find(user_id)
+
+ mail(to: requester.notification_email,
+ subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
+ end
+
+ def member_invited_email(member_source_type, member_id, token)
+ @member_source_type = member_source_type
+ @member_id = member_id
+ @token = token
+
+ mail(to: member.invite_email,
+ subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
+ end
+
+ def member_invite_accepted_email(member_source_type, member_id)
+ @member_source_type = member_source_type
+ @member_id = member_id
+ return unless member.created_by
+
+ mail(to: member.created_by.notification_email,
+ subject: subject('Invitation accepted'))
+ end
+
+ def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
+ return unless created_by_id
+
+ @member_source_type = member_source_type
+ @member_source = member_source_class.find(source_id)
+ @invite_email = invite_email
+ inviter = User.find(created_by_id)
+
+ mail(to: inviter.notification_email,
+ subject: subject('Invitation declined'))
+ end
+
+ def member
+ @member ||= Member.find(@member_id)
+ end
+
+ def member_source
+ @member_source ||= member.source
+ end
+
+ private
+
+ def member_source_class
+ @member_source_type.classify.constantize
+ end
+ end
+end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index fdf1e9f5afc..e0af7081411 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,55 +1,5 @@
module Emails
module Projects
- def project_access_granted_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
- @project = @project_member.project
-
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.user
-
- mail(to: @project_member.user.notification_email,
- subject: subject("Access to project was granted"))
- end
-
- def project_member_invited_email(project_member_id, token)
- @project_member = ProjectMember.find project_member_id
- @project = @project_member.project
- @token = token
-
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.user
-
- mail(to: @project_member.invite_email,
- subject: "Invitation to join project #{@project.name_with_namespace}")
- end
-
- def project_invite_accepted_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
- return if @project_member.created_by.nil?
-
- @project = @project_member.project
-
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.created_by
-
- mail(to: @project_member.created_by.notification_email,
- subject: subject("Invitation accepted"))
- end
-
- def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
- return if created_by_id.nil?
-
- @project = Project.find(project_id)
- @current_user = @created_by = User.find(created_by_id)
- @access_level = access_level
- @invite_email = invite_email
-
- @target_url = namespace_project_url(@project.namespace, @project)
-
- mail(to: @created_by.notification_email,
- subject: subject("Invitation declined"))
- end
-
def project_was_moved_email(project_id, user_id, old_path_with_namespace)
@current_user = @user = User.find user_id
@project = Project.find project_id
@@ -59,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/mailers/notify.rb b/app/mailers/notify.rb
index 1c663bdd521..0cc709f68e4 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,13 +6,15 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
- include Emails::Groups
include Emails::Builds
+ include Emails::Members
add_template_helper MergeRequestsHelper
add_template_helper DiffHelper
add_template_helper BlobHelper
add_template_helper EmailsHelper
+ add_template_helper MembersHelper
+ add_template_helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 44515550d9e..9c58b956007 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -9,7 +9,6 @@ class Ability
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
when Issue then issue_abilities(user, subject)
- when ExternalIssue then external_issue_abilities(user, subject)
when Note then note_abilities(user, subject)
when ProjectSnippet then project_snippet_abilities(user, subject)
when PersonalSnippet then personal_snippet_abilities(user, subject)
@@ -19,6 +18,7 @@ class Ability
when GroupMember then group_member_abilities(user, subject)
when ProjectMember then project_member_abilities(user, subject)
when User then user_abilities
+ when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project)
else []
end.concat(global_abilities(user))
end
@@ -187,6 +187,8 @@ class Ability
project_report_rules
elsif team.guest?(user)
project_guest_rules
+ else
+ []
end
end
@@ -228,6 +230,8 @@ class Ability
:read_build,
:read_container_image,
:read_pipeline,
+ :read_environment,
+ :read_deployment
]
end
@@ -246,6 +250,8 @@ class Ability
:push_code,
:create_container_image,
:update_container_image,
+ :create_environment,
+ :create_deployment
]
end
@@ -263,6 +269,8 @@ class Ability
@project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
+ :update_environment,
+ :update_deployment,
:admin_milestone,
:admin_project_snippet,
:admin_project_member,
@@ -273,7 +281,9 @@ class Ability
:admin_commit_status,
:admin_build,
:admin_container_image,
- :admin_pipeline
+ :admin_pipeline,
+ :admin_environment,
+ :admin_deployment
]
end
@@ -317,6 +327,8 @@ class Ability
unless project.builds_enabled
rules += named_abilities('build')
rules += named_abilities('pipeline')
+ rules += named_abilities('environment')
+ rules += named_abilities('deployment')
end
unless project.container_registry_enabled
@@ -511,10 +523,6 @@ class Ability
end
end
- def external_issue_abilities(user, subject)
- project_abilities(user, subject.project)
- end
-
private
def restricted_public_level?
@@ -533,7 +541,7 @@ class Ability
def filter_confidential_issues_abilities(user, issue, rules)
return rules if user.admin? || !issue.confidential?
- unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
+ unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER)
rules.delete(:admin_issue)
rules.delete(:read_issue)
rules.delete(:update_issue)
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/blob.rb b/app/models/blob.rb
index 0fea6b7f576..4279ea2ce57 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -24,7 +24,7 @@ class Blob < SimpleDelegator
end
def only_display_raw?
- size && size > 5.megabytes
+ size && truncated?
end
def svg?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6a64ca451f7..2b0bec33131 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -11,6 +11,8 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
+ scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
+ scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
@@ -38,7 +40,7 @@ module Ci
new_build.save
end
- def retry(build)
+ def retry(build, user = nil)
new_build = Ci::Build.new(status: 'pending')
new_build.ref = build.ref
new_build.tag = build.tag
@@ -52,6 +54,7 @@ module Ci
new_build.stage = build.stage
new_build.stage_idx = build.stage_idx
new_build.trigger_request = build.trigger_request
+ new_build.user = user
new_build.save
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
new_build
@@ -73,6 +76,17 @@ module Ci
build.update_coverage
build.execute_hooks
end
+
+ after_transition any => [:success] do |build|
+ if build.environment.present?
+ service = CreateDeploymentService.new(build.project, build.user,
+ environment: build.environment,
+ sha: build.sha,
+ ref: build.ref,
+ tag: build.tag)
+ service.execute(build)
+ end
+ end
end
def retryable?
@@ -83,10 +97,6 @@ module Ci
!self.pipeline.statuses.latest.include?(self)
end
- def retry
- Ci::Build.retry(self)
- end
-
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -290,18 +300,12 @@ module Ci
project.valid_runners_token? token
end
- def can_be_served?(runner)
- return false unless has_tags? || runner.run_untagged?
-
- (tag_list - runner.tag_list).empty?
- end
-
def has_tags?
tag_list.any?
end
def any_runners_online?
- project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
+ project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
end
def stuck?
@@ -317,7 +321,7 @@ module Ci
end
def artifacts?
- artifacts_file.exists?
+ !artifacts_expired? && artifacts_file.exists?
end
def artifacts_metadata?
@@ -328,11 +332,16 @@ module Ci
Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry
end
+ def erase_artifacts!
+ remove_artifacts_file!
+ remove_artifacts_metadata!
+ save
+ end
+
def erase(opts = {})
return false unless erasable?
- remove_artifacts_file!
- remove_artifacts_metadata!
+ erase_artifacts!
erase_trace!
update_erased!(opts[:erased_by])
end
@@ -345,6 +354,25 @@ module Ci
!self.erased_at.nil?
end
+ def artifacts_expired?
+ artifacts_expire_at && artifacts_expire_at < Time.now
+ end
+
+ def artifacts_expire_in
+ artifacts_expire_at - Time.now if artifacts_expire_at
+ end
+
+ def artifacts_expire_in=(value)
+ self.artifacts_expire_at =
+ if value
+ Time.now + ChronicDuration.parse(value)
+ end
+ end
+
+ def keep_artifacts!
+ self.update(artifacts_expire_at: nil)
+ end
+
private
def erase_trace!
@@ -352,7 +380,7 @@ module Ci
end
def update_erased!(user = nil)
- self.update(erased_by: user, erased_at: Time.now)
+ self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
def yaml_variables
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 9b5b46f4928..ca5a685dd11 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -37,22 +37,22 @@ module Ci
end
def git_author_name
- commit_data.author_name if commit_data
+ commit.try(:author_name)
end
def git_author_email
- commit_data.author_email if commit_data
+ commit.try(:author_email)
end
def git_commit_message
- commit_data.message if commit_data
+ commit.try(:message)
end
def short_sha
Ci::Pipeline.truncate_sha(sha)
end
- def commit_data
+ def commit
@commit ||= project.commit(sha)
rescue
nil
@@ -76,8 +76,10 @@ module Ci
builds.running_or_pending.each(&:cancel)
end
- def retry_failed
- builds.latest.failed.select(&:retryable?).each(&:retry)
+ def retry_failed(user)
+ builds.latest.failed.select(&:retryable?).each do |build|
+ Ci::Build.retry(build, user)
+ end
end
def latest?
@@ -92,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)
@@ -113,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
@@ -137,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
@@ -161,8 +166,27 @@ module Ci
git_commit_message =~ /(\[ci skip\])/ if git_commit_message
end
+ def environments
+ 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?
@@ -175,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/ci/runner.rb b/app/models/ci/runner.rb
index adb65292208..b64ec79ec2b 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -4,7 +4,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
- FORM_EDITABLE = %i[description tag_list active run_untagged]
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked]
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
@@ -26,6 +26,13 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
+ scope :assignable_for, ->(project) do
+ # FIXME: That `to_sql` is needed to workaround a weird Rails bug.
+ # Without that, placeholders would miss one and couldn't match.
+ where(locked: false).
+ where.not("id IN (#{project.runners.select(:id).to_sql})").specific
+ end
+
validate :tag_constraints
acts_as_taggable
@@ -56,7 +63,7 @@ module Ci
def assign_to(project, current_user = nil)
self.is_shared = false if shared?
self.save
- project.runner_projects.create!(runner_id: self.id)
+ project.runner_projects.create(runner_id: self.id)
end
def display_name
@@ -91,6 +98,10 @@ module Ci
!shared?
end
+ def can_pick?(build)
+ assignable_for?(build.project) && accepting_tags?(build)
+ end
+
def only_for?(project)
projects == [project]
end
@@ -111,5 +122,13 @@ module Ci
'can not be empty when runner is not allowed to pick untagged jobs')
end
end
+
+ def assignable_for?(project)
+ !locked? || projects.exists?(id: project.id)
+ end
+
+ def accepting_tags?(build)
+ (run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
+ end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index d69d518fadd..174ccbaea6c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -271,6 +271,32 @@ class Commit
merged_merge_request ? 'merge request' : 'commit'
end
+ # Get the URI type of the given path
+ #
+ # Used to build URLs to files in the repository in GFM.
+ #
+ # path - String path to check
+ #
+ # Examples:
+ #
+ # uri_type('doc/README.md') # => :blob
+ # uri_type('doc/logo.png') # => :raw
+ # uri_type('doc/api') # => :tree
+ # uri_type('not/found') # => :nil
+ #
+ # Returns a symbol
+ def uri_type(path)
+ entry = @raw.tree.path(path)
+ if entry[:type] == :blob
+ blob = Gitlab::Git::Blob.new(name: entry[:name])
+ blob.image? ? :raw : :blob
+ else
+ entry[:type]
+ end
+ rescue Rugged::TreeError
+ nil
+ end
+
private
def repo_changes
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e53c483b904..e437e3417a8 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,9 @@ class CommitStatus < ActiveRecord::Base
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user
- validates :pipeline, presence: true
+ delegate :commit, to: :pipeline
+
+ validates :pipeline, presence: true, unless: :importing?
validates_presence_of :name
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
new file mode 100644
index 00000000000..eedd32a729f
--- /dev/null
+++ b/app/models/concerns/access_requestable.rb
@@ -0,0 +1,16 @@
+# == AccessRequestable concern
+#
+# Contains functionality related to objects that can receive request for access.
+#
+# Used by Project, and Group.
+#
+module AccessRequestable
+ extend ActiveSupport::Concern
+
+ def request_access(user)
+ members.create(
+ access_level: Gitlab::Access::DEVELOPER,
+ user: user,
+ requested_at: Time.now.utc)
+ end
+end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index aa4b4201250..539c7c31e30 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -5,7 +5,7 @@ module Awardable
has_many :award_emoji, as: :awardable, dependent: :destroy
if self < Participable
- participant :award_emoji
+ participant :award_emoji_with_associations
end
end
@@ -34,8 +34,12 @@ module Awardable
end
end
+ def award_emoji_with_associations
+ award_emoji.includes(:user)
+ end
+
def grouped_awards(with_thumbs: true)
- awards = award_emoji.group_by(&:name)
+ awards = award_emoji_with_associations.group_by(&:name)
if with_thumbs
awards[AwardEmoji::UPVOTE_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/concerns/participable.rb b/app/models/concerns/participable.rb
index 9056722f45e..9822844357d 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -53,6 +53,16 @@ module Participable
#
# Returns an Array of User instances.
def participants(current_user = nil)
+ @participants ||= Hash.new do |hash, user|
+ hash[user] = raw_participants(user)
+ end
+
+ @participants[current_user]
+ end
+
+ private
+
+ def raw_participants(current_user = nil)
current_user ||= author
ext = Gitlab::ReferenceExtractor.new(project, current_user)
participants = Set.new
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index ce064f675ae..dee940a3f88 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -49,6 +49,10 @@ module Referable
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
+ def reference_valid?(reference)
+ true
+ end
+
def link_reference_pattern(route, pattern)
%r{
(?<url>
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
new file mode 100644
index 00000000000..e498ca96e3c
--- /dev/null
+++ b/app/models/deployment.rb
@@ -0,0 +1,29 @@
+class Deployment < ActiveRecord::Base
+ include InternalId
+
+ belongs_to :project, required: true, validate: true
+ belongs_to :environment, required: true, validate: true
+ belongs_to :user
+ belongs_to :deployable, polymorphic: true
+
+ validates :sha, presence: true
+ validates :ref, presence: true
+
+ delegate :name, to: :environment, prefix: true
+
+ def commit
+ project.commit(sha)
+ end
+
+ def commit_title
+ commit.try(:title)
+ end
+
+ def short_sha
+ Commit.truncate_sha(sha)
+ end
+
+ def last?
+ self == environment.last_deployment
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
new file mode 100644
index 00000000000..ac3a571a1f3
--- /dev/null
+++ b/app/models/environment.rb
@@ -0,0 +1,16 @@
+class Environment < ActiveRecord::Base
+ belongs_to :project, required: true, validate: true
+
+ has_many :deployments
+
+ validates :name,
+ presence: true,
+ uniqueness: { scope: :project_id },
+ length: { within: 0..255 },
+ format: { with: Gitlab::Regex.environment_name_regex,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ def last_deployment
+ deployments.last
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index aec92e335e6..e66e04371b2 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -3,11 +3,18 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
+ include AccessRequestable
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
alias_method :members, :group_members
- has_many :users, through: :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
@@ -58,6 +65,10 @@ class Group < Namespace
"#{self.class.reference_prefix}#{name}"
end
+ def web_url
+ Gitlab::Routing.url_helpers.group_url(self)
+ end
+
def human_name
name
end
@@ -83,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/issue.rb b/app/models/issue.rb
index 235922710ad..3c5859194b4 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -51,10 +51,18 @@ class Issue < ActiveRecord::Base
end
def self.visible_to_user(user)
- return where(confidential: false) if user.blank?
+ return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
return all if user.admin?
- where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
+ where('
+ issues.confidential IS NULL
+ OR issues.confidential IS FALSE
+ OR (issues.confidential = TRUE
+ AND (issues.author_id = :user_id
+ OR issues.assignee_id = :user_id
+ OR issues.project_id IN(:project_ids)))',
+ user_id: user.id,
+ project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
def self.reference_prefix
@@ -75,6 +83,10 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
+ def self.reference_valid?(reference)
+ reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
+ end
+
def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
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/key.rb b/app/models/key.rb
index 0532e84f47d..b9bc38a0436 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -9,7 +9,7 @@ class Key < ActiveRecord::Base
before_validation :strip_white_space, :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 }
- validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true
+ validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
validates :key, format: { without: /\n|\r/, message: 'should be a single line' }
validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' }
diff --git a/app/models/member.rb b/app/models/member.rb
index d3060f07fc0..c74a16367db 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
@@ -26,20 +27,27 @@ class Member < ActiveRecord::Base
allow_nil: true
}
- scope :invite, -> { where(user_id: nil) }
- scope :non_invite, -> { where("user_id IS NOT NULL") }
+ scope :invite, -> { where.not(invite_token: nil) }
+ scope :non_invite, -> { where(invite_token: nil) }
+ scope :request, -> { where.not(requested_at: nil) }
+ scope :non_request, -> { where(requested_at: nil) }
+ scope :non_pending, -> { non_request.non_invite }
+
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) }
+ scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
- after_create :send_invite, if: :invite?
- after_create :create_notification_setting, unless: :invite?
- after_create :post_create_hook, unless: :invite?
- after_update :post_update_hook, unless: :invite?
- after_destroy :post_destroy_hook, unless: :invite?
+
+ 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?
delegate :name, :username, :email, to: :user, prefix: true
@@ -96,10 +104,31 @@ class Member < ActiveRecord::Base
end
end
+ def real_source_type
+ source_type
+ end
+
def invite?
self.invite_token.present?
end
+ def request?
+ requested_at.present?
+ end
+
+ def pending?
+ invite? || request?
+ end
+
+ def accept_request
+ return false unless request?
+
+ updated = self.update(requested_at: nil)
+ after_accept_request if updated
+
+ updated
+ end
+
def accept_invite!(new_user)
return false unless invite?
@@ -157,6 +186,10 @@ class Member < ActiveRecord::Base
# override in subclass
end
+ def send_request
+ notification_service.new_access_request(self)
+ end
+
def post_create_hook
system_hook_service.execute_hooks_for(self, :create)
end
@@ -177,6 +210,10 @@ class Member < ActiveRecord::Base
# override in subclass
end
+ def after_accept_request
+ post_create_hook
+ end
+
def system_hook_service
SystemHooksService.new
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index f63a0debf1a..2f13d339c89 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -8,9 +8,6 @@ class GroupMember < Member
validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) }
- scope :with_group, ->(group) { where(source_id: group.id) }
- scope :with_user, ->(user) { where(user_id: user.id) }
-
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -23,6 +20,11 @@ class GroupMember < Member
access_level
end
+ # Because source_type is `Namespace`...
+ def real_source_type
+ 'Group'
+ end
+
private
def send_invite
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 46955b430f3..e9d3a82ba15 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -11,8 +11,6 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
- scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
- scope :with_user, ->(user) { where(user_id: user.id) }
before_destroy :delete_member_todos
@@ -84,7 +82,7 @@ class ProjectMember < Member
Gitlab::Access.sym_options
end
- def access_roles
+ def access_level_roles
Gitlab::Access.options
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7b8858b24d6..36bc98bdb1e 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) }
@@ -132,6 +133,10 @@ class MergeRequest < ActiveRecord::Base
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end
+ def self.reference_valid?(reference)
+ reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
+ end
+
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
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 585d8c4ad84..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
@@ -88,22 +89,9 @@ class Note < ActiveRecord::Base
table = arel_table
pattern = "%#{query}%"
- found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id').
- where(table[:note].matches(pattern))
-
- if as_user
- found_notes.where('
- issues.confidential IS NULL
- OR issues.confidential IS FALSE
- OR (issues.confidential IS TRUE
- AND (issues.author_id = :user_id
- OR issues.assignee_id = :user_id
- OR issues.project_id IN(:project_ids)))',
- user_id: as_user.id,
- project_ids: as_user.authorized_projects.select(:id))
- else
- found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE')
- end
+ Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
+ where(table[:note].matches(pattern)).
+ merge(Issue.visible_to_user(as_user))
end
end
@@ -200,6 +188,10 @@ class Note < ActiveRecord::Base
award_emoji_supported? && contains_emoji_only?
end
+ def emoji_awardable?
+ !system?
+ end
+
def clear_blank_line_code!
self.line_code = nil if self.line_code.blank?
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 43acb4c5a7b..d41fc7073c6 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -30,7 +30,7 @@ class NotificationSetting < ActiveRecord::Base
store :events, accessors: EMAIL_EVENTS, coder: JSON
- before_save :set_events
+ before_create :set_events
before_save :events_to_boolean
def self.find_or_create_for(source)
@@ -45,7 +45,7 @@ class NotificationSetting < ActiveRecord::Base
# Set all event attributes to false when level is not custom or being initialized for UX reasons
def set_events
- return if custom? || persisted?
+ return if custom?
EMAIL_EVENTS.each do |event|
events[event] = false
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 e2f7ffe493c..ca3bc04e2dd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
+ include AccessRequestable
include Referable
include Sortable
include AfterCommitQueue
@@ -80,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"
@@ -102,8 +103,9 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
- has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
- has_many :users, through: :project_members
+ has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
+ alias_method :members, :project_members
+ has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members
has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
@@ -125,6 +127,8 @@ class Project < ActiveRecord::Base
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+ has_many :environments, dependent: :destroy
+ has_many :deployments, dependent: :destroy
accepts_nested_attributes_for :variables, allow_destroy: true
@@ -146,7 +150,6 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
- validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
@@ -259,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.
@@ -348,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
@@ -451,7 +475,7 @@ class Project < ActiveRecord::Base
end
def import?
- external_import? || forked?
+ external_import? || forked? || gitlab_project_import?
end
def no_import?
@@ -482,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
@@ -589,10 +617,6 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
- def can_have_issues_tracker_id?
- self.issues_enabled && !self.default_issues_tracker?
- end
-
def build_missing_services
services_templates = Service.where(template: true)
@@ -685,16 +709,6 @@ class Project < ActiveRecord::Base
end
end
- def project_member_by_name_or_email(name = nil, email = nil)
- user = users.find_by('name like ? or email like ?', name, email)
- project_members.where(user: user) if user
- end
-
- # Get Team Member record by user id
- def project_member_by_id(user_id)
- project_members.find_by(user_id: user_id)
- end
-
def name_with_namespace
@name_with_namespace ||= begin
if namespace
@@ -704,6 +718,7 @@ class Project < ActiveRecord::Base
end
end
end
+ alias_method :human_name, :name_with_namespace
def path_with_namespace
if namespace
@@ -1090,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/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 1d1780dcfbf..b5c76e4d4fe 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,6 +1,4 @@
class BambooService < CiService
- include HTTParty
-
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated?
@@ -61,18 +59,7 @@ class BambooService < CiService
end
def build_info(sha)
- url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s
-
- if username.blank? && password.blank?
- @response = HTTParty.get(url, verify: false)
- else
- url << '&os_authType=basic'
- auth = {
- username: username,
- password: password
- }
- @response = HTTParty.get(url, verify: false, basic_auth: auth)
- end
+ @response = get_path("rest/api/latest/result?label=#{sha}")
end
def build_page(sha, ref)
@@ -80,11 +67,11 @@ class BambooService < CiService
if @response.code != 200 || @response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
- URI.join(bamboo_url, "/browse/#{build_key}").to_s
+ URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key']
- URI.join(bamboo_url, "/browse/#{result_key}").to_s
+ URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end
end
@@ -112,8 +99,27 @@ class BambooService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
- # Bamboo requires a GET and does not take any data.
- url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s
- self.class.get(url, verify: false)
+ get_path("updateAndBuild.action?buildKey=#{build_key}")
+ end
+
+ private
+
+ def build_url(path)
+ URI.join("#{bamboo_url}/", path).to_s
+ end
+
+ def get_path(path)
+ url = build_url(path)
+
+ if username.blank? && password.blank?
+ HTTParty.get(url, verify: false)
+ else
+ url << '&os_authType=basic'
+ HTTParty.get(url, verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
+ end
end
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 6ae9b16d3ce..87ecb3b8b86 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -38,9 +38,9 @@ class IssueTrackerService < Service
if enabled_in_gitlab_config
self.properties = {
title: issues_tracker['title'],
- project_url: add_issues_tracker_id(issues_tracker['project_url']),
- issues_url: add_issues_tracker_id(issues_tracker['issues_url']),
- new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url'])
+ project_url: issues_tracker['project_url'],
+ issues_url: issues_tracker['issues_url'],
+ new_issue_url: issues_tracker['new_issue_url']
}
else
self.properties = {}
@@ -83,16 +83,4 @@ class IssueTrackerService < Service
def issues_tracker
Gitlab.config.issues_tracker[to_param]
end
-
- def add_issues_tracker_id(url)
- if self.project
- id = self.project.issues_tracker_id
-
- if id
- url = url.gsub(":issues_tracker_id", id)
- end
- end
-
- url
- end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b0dcb52eba1..a4a967c9bc9 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -1,6 +1,4 @@
class TeamcityService < CiService
- include HTTParty
-
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated?
@@ -64,15 +62,7 @@ class TeamcityService < CiService
end
def build_info(sha)
- url = URI.join(
- teamcity_url,
- "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}"
- ).to_s
- auth = {
- username: username,
- password: password
- }
- @response = HTTParty.get(url, verify: false, basic_auth: auth)
+ @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
end
def build_page(sha, ref)
@@ -81,14 +71,11 @@ class TeamcityService < CiService
if @response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
- URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s
+ build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = @response['build']['id']
- URI.join(
- teamcity_url,
- "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}"
- ).to_s
+ build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end
@@ -123,8 +110,8 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref])
- self.class.post(
- URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s,
+ HTTParty.post(
+ build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=\"#{branch}\">"\
"<buildType id=\"#{build_type}\"/>"\
'</build>',
@@ -132,4 +119,18 @@ class TeamcityService < CiService
basic_auth: auth
)
end
+
+ private
+
+ def build_url(path)
+ URI.join("#{teamcity_url}/", path).to_s
+ end
+
+ def get_path(path)
+ HTTParty.get(build_url(path), verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
+ end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 70a8bbaba65..73e736820af 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -21,23 +21,13 @@ class ProjectTeam
end
end
- def find(user_id)
- user = project.users.find_by(id: user_id)
-
- if group
- user ||= group.users.find_by(id: user_id)
- end
-
- user
- end
-
def find_member(user_id)
- member = project.project_members.find_by(user_id: user_id)
+ member = project.members.non_request.find_by(user_id: user_id)
# If user is not in project members
# we should check for group membership
if group && !member
- member = group.group_members.find_by(user_id: user_id)
+ member = group.members.non_request.find_by(user_id: user_id)
end
member
@@ -61,13 +51,10 @@ class ProjectTeam
ProjectMember.truncate_team(project)
end
- def users
- members
- end
-
def members
@members ||= fetch_members
end
+ alias_method :users, :members
def guests
@guests ||= fetch_members(:guests)
@@ -131,8 +118,14 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user_id)
- !!find_member(user_id)
+ def member?(user, min_member_access = nil)
+ member = !!find_member(user.id)
+
+ if min_member_access
+ member && max_member_access(user.id) >= min_member_access
+ else
+ member
+ end
end
def human_max_access(user_id)
@@ -144,7 +137,7 @@ class ProjectTeam
def max_member_access(user_id)
access = []
- project.project_members.each do |member|
+ project.members.non_request.each do |member|
if member.user_id == user_id
access << member.access_field if member.access_field
break
@@ -152,7 +145,7 @@ class ProjectTeam
end
if group
- group.group_members.each do |member|
+ group.members.non_request.each do |member|
if member.user_id == user_id
access << member.access_field if member.access_field
break
@@ -167,6 +160,7 @@ class ProjectTeam
access.compact.max
end
+ private
def max_invited_level(user_id)
project.project_group_links.map do |group_link|
@@ -183,17 +177,15 @@ class ProjectTeam
end.compact.max
end
- private
-
def fetch_members(level = nil)
- project_members = project.project_members
- group_members = group ? group.group_members : []
+ project_members = project.members.non_request
+ group_members = group ? group.members.non_request : []
invited_members = []
if project.invited_groups.any? && project.allowed_to_share_with_group?
project.project_group_links.each do |group_link|
invited_group = group_link.group
- im = invited_group.group_members
+ im = invited_group.members.non_request
if level
int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1ab163510bf..221c87164ca 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -191,8 +191,12 @@ class Repository
end
end
+ def ref_names
+ branch_names + tag_names
+ end
+
def branch_names
- cache.fetch(:branch_names) { branches.map(&:name) }
+ @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
end
def branch_exists?(branch_name)
@@ -243,7 +247,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
@@ -267,6 +271,7 @@ class Repository
def expire_branches_cache
cache.expire(:branch_names)
+ @branch_names = nil
@local_branches = nil
end
@@ -332,10 +337,6 @@ class Repository
@lookup_cache ||= {}
end
- def expire_branch_names
- cache.expire(:branch_names)
- end
-
def expire_avatar_cache(branch_name = nil, revision = nil)
# Avatars are pulled from the default branch, thus if somebody pushes to a
# different branch there's no need to expire anything.
@@ -446,7 +447,7 @@ class Repository
def blob_at(sha, path)
unless Gitlab::Git.blank_ref?(sha)
- Gitlab::Git::Blob.find(self, sha, path)
+ Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
end
@@ -598,6 +599,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 +1011,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/todo.rb b/app/models/todo.rb
index 3a091373329..2792fa9b9a8 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
+ MARKED = 4
belongs_to :author, class_name: "User"
belongs_to :note
diff --git a/app/models/user.rb b/app/models/user.rb
index 7afbfbf112a..876ccc69d8d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -51,13 +51,13 @@ 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
# Groups
has_many :members, dependent: :destroy
- has_many :project_members, source: 'ProjectMember'
- has_many :group_members, source: 'GroupMember'
+ has_many :group_members, dependent: :destroy, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
@@ -65,13 +65,13 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
+ has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
- has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
@@ -268,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
@@ -405,8 +410,8 @@ class User < ActiveRecord::Base
end
# Returns projects user is authorized to access.
- def authorized_projects
- Project.where("projects.id IN (#{projects_union.to_sql})")
+ def authorized_projects(min_access_level = nil)
+ Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
end
def viewable_starred_projects
@@ -482,9 +487,8 @@ class User < ActiveRecord::Base
events.recent.find do |event|
project = Project.find_by_id(event.project_id)
next unless project
- repo = project.repository
- if repo.branch_names.include?(event.branch_name)
+ if project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
where(source_project_id: project.id,
source_branch: event.branch_name)
@@ -822,13 +826,38 @@ 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
- Gitlab::SQL::Union.new([personal_projects.select(:id),
- groups_projects.select(:id),
- projects.select(:id),
- groups.joins(:shared_projects).select(:project_id)])
+ def projects_union(min_access_level = nil)
+ relations = [personal_projects.select(:id),
+ groups_projects.select(:id),
+ projects.select(:id),
+ groups.joins(:shared_projects).select(:project_id)]
+
+
+ if min_access_level
+ scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
+ relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
+ end
+
+ Gitlab::SQL::Union.new(relations)
end
def ci_projects_union
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 64bcdac5c65..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,33 +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)
+ 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..9a187f5d694 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -7,17 +7,21 @@ 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)
+ current_runner.can_pick?(build)
end
if build
@@ -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/create_deployment_service.rb b/app/services/create_deployment_service.rb
new file mode 100644
index 00000000000..efeb9df9527
--- /dev/null
+++ b/app/services/create_deployment_service.rb
@@ -0,0 +1,18 @@
+require_relative 'base_service'
+
+class CreateDeploymentService < BaseService
+ def execute(deployable = nil)
+ environment = project.environments.find_or_create_by(
+ name: params[:environment]
+ )
+
+ project.deployments.create(
+ environment: environment,
+ ref: params[:ref],
+ tag: params[:tag],
+ sha: params[:sha],
+ user: current_user,
+ deployable: deployable
+ )
+ 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/members/destroy_service.rb b/app/services/members/destroy_service.rb
new file mode 100644
index 00000000000..15358f80208
--- /dev/null
+++ b/app/services/members/destroy_service.rb
@@ -0,0 +1,21 @@
+module Members
+ class DestroyService < BaseService
+ attr_accessor :member, :current_user
+
+ def initialize(member, user)
+ @member, @current_user = member, user
+ end
+
+ def execute
+ unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ member.destroy
+
+ if member.request? && member.user != current_user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 1b48899bb0a..7fe57747265 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -83,7 +83,7 @@ module MergeRequests
closes_issue = "Closes ##{iid}"
if merge_request.description.present?
- merge_request.description << closes_issue.prepend("\n")
+ merge_request.description += closes_issue.prepend("\n")
else
merge_request.description = closes_issue
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 9decebe330c..590350a11e5 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -181,16 +181,27 @@ class NotificationService
end
end
+ # Members
+ def new_access_request(member)
+ mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
+ end
+
+ def decline_access_request(member)
+ mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
+ end
+
+ # Project invite
def invite_project_member(project_member, token)
- mailer.project_member_invited_email(project_member.id, token).deliver_later
+ mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end
def accept_project_invite(project_member)
- mailer.project_invite_accepted_email(project_member.id).deliver_later
+ mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
end
def decline_project_invite(project_member)
- mailer.project_invite_declined_email(
+ mailer.member_invite_declined_email(
+ project_member.real_source_type,
project_member.project.id,
project_member.invite_email,
project_member.access_level,
@@ -199,23 +210,25 @@ class NotificationService
end
def new_project_member(project_member)
- mailer.project_access_granted_email(project_member.id).deliver_later
+ mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
def update_project_member(project_member)
- mailer.project_access_granted_email(project_member.id).deliver_later
+ mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
+ # Group invite
def invite_group_member(group_member, token)
- mailer.group_member_invited_email(group_member.id, token).deliver_later
+ mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
end
def accept_group_invite(group_member)
- mailer.group_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)
- mailer.group_invite_declined_email(
+ mailer.member_invite_declined_email(
+ group_member.real_source_type,
group_member.group.id,
group_member.invite_email,
group_member.access_level,
@@ -224,11 +237,11 @@ class NotificationService
end
def new_group_member(group_member)
- mailer.group_access_granted_email(group_member.id).deliver_later
+ mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def update_group_member(group_member)
- mailer.group_access_granted_email(group_member.id).deliver_later
+ mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def project_was_moved(project, old_path_with_namespace)
@@ -254,6 +267,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/group users with CUSTOM notification level
@@ -280,6 +301,7 @@ class NotificationService
users_with_project_level_global = notification_settings_for(project, :global)
users_with_group_level_global = notification_settings_for(project.group, :global)
+
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
@@ -495,6 +517,7 @@ class NotificationService
recipients = target.participants(current_user)
recipients = add_project_watchers(recipients, project)
+
recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
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..80c7193efcb
--- /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)
+ 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 8e03ff8ddde..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,14 +137,30 @@ 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
+ def mark_todo(issuable, current_user)
+ attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED)
+ create_todos(current_user, attributes)
end
private
def create_todos(users, attributes)
- Array(users).each do |user|
+ 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
@@ -155,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/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index d88f3ad314d..dc083e50178 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -46,7 +46,7 @@
Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
.form-actions
- = f.submit 'Save', class: 'btn btn-save'
+ = f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
= link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
index dd4a64e80bc..6c51639b840 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview.html.haml
@@ -1,29 +1,9 @@
- page_title "Preview | Appearance"
-%h3.page-title
- Appearance settings - Preview
-%hr
+.login-box
+ .login-heading
+ %h3 Existing user? Sign in
+ %form
+ = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
+ = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
+ = button_tag "Sign in", class: "btn-create btn"
-.ui-box
- .title
- Sign-in page
- %div
- .login-page
- .container
- .content
- .login-title
- %h1= brand_title
- %hr
- .container
- .content
- .row
- .col-sm-7
- .brand-image
- = brand_image
- .brand_text
- = brand_text
- .col-sm-4
- .login-box
- %h3.page-title Sign in
- = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
- = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
- = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml
index 089e8e4cb7a..454b779842c 100644
--- a/app/views/admin/appearances/show.html.haml
+++ b/app/views/admin/appearances/show.html.haml
@@ -1,7 +1,9 @@
- page_title "Appearance"
+
%h3.page-title
Appearance settings
%p.light
You can modify the look and feel of GitLab here
+%hr
= render 'form'
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index e9c7ca9d5aa..ecc46d86afe 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Settings"
+
%h3.page-title Settings
%hr
= render 'form'
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..b74da64f82e
--- /dev/null
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -0,0 +1,26 @@
+.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
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
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/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f309e80a39a..50770465f07 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -88,28 +88,17 @@
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag 'Add users to group', class: "btn btn-create"
+
+ = render 'shared/members/requests', membership_source: @group, members: @members.request
+
.panel.panel-default
.panel-heading
- %h3.panel-title
- Members
- %span.badge
- #{@group.group_members.count}
- %ul.well-list.group-users-list
- - @members.each do |member|
- - user = member.user
- %li{class: dom_class(member), id: (dom_id(user) if user)}
- .list-item-name
- - if user
- %strong
- = link_to user.name, admin_user_path(user)
- - else
- %strong
- = member.invite_email
- (invited)
- %span.pull-right.light
- = member.human_access
- - if can?(current_user, :destroy_group_member, member)
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- %i.fa.fa-minus.fa-inverse
+ %strong= @group.name
+ group members
+ %span.badge= @group.members.non_request.size
+ .pull-right
+ = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@group, :members]), class: "btn btn-xs"
+ %ul.well-list.group-users-list.content-list
+ = render partial: 'shared/members/member', collection: @members.non_request, as: :member, locals: { show_controls: false }
.panel-footer
- = paginate @members, param_name: 'members_page', theme: 'gitlab'
+ = paginate @members.non_request, param_name: 'members_page', 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/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 73986d21bcf..461d588415d 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -135,44 +135,27 @@
- if @group
.panel.panel-default
.panel-heading
- %strong #{@group.name}
- group members (#{@group.group_members.count})
+ %strong= @group.name
+ group members
+ %span.badge= @group_members.non_request.size
.pull-right
= link_to admin_group_path(@group), class: 'btn btn-xs' do
- %i.fa.fa-pencil-square-o
- %ul.well-list
- - @group_members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false
+ = icon('pencil-square-o', text: 'Manage Access')
+ %ul.well-list.content-list
+ = render partial: 'shared/members/member', collection: @group_members.non_request, as: :member, locals: { show_controls: false }
.panel-footer
- = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
+ = paginate @group_members.non_request, param_name: 'group_members_page', theme: 'gitlab'
+
+ = render 'shared/members/requests', membership_source: @project, members: @project_members.request
.panel.panel-default
.panel-heading
- Project members
- %small
- (#{@project.users.count})
+ %strong= @project.name
+ project members
+ %span.badge= @project.users.size
.pull-right
- = link_to namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-xs" do
- %i.fa.fa-pencil-square-o
- Manage Access
- %ul.well-list.project_members
- - @project_members.each do |project_member|
- - user = project_member.user
- %li.project_member
- .list-item-name
- - if user
- %strong
- = link_to user.name, admin_user_path(user)
- - else
- %strong
- = project_member.invite_email
- (invited)
- .pull-right
- - if project_member.owner?
- %span.light Owner
- - else
- %span.light= project_member.human_access
- = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
- %i.fa.fa-times
+ = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@project, :members]), class: "btn btn-xs"
+ %ul.well-list.project_members.content-list
+ = render partial: 'shared/members/member', collection: @project_members.non_request, as: :member, locals: { show_controls: false }
.panel-footer
- = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
+ = paginate @project_members.non_request, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 2dad64b8d0f..5eff77aff2d 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,67 +1,73 @@
-%p.lead.prepend-top-default
- %span
- To register a new runner you should enter the following registration token.
- With this token the runner will request a unique runner token and use that for future communication.
- Registration token is
- %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
+- @no_container = true
+= render "admin/dashboard/head"
-.bs-callout.clearfix
- .pull-left
- %p
- You can reset runners registration token by pressing a button below.
- %p
- = button_to reset_runners_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: 'Are you sure you want to reset registration token?' } do
- = icon('refresh')
- Reset runners registration token
+%div{ class: (container_class) }
+
+ %p.prepend-top-default
+ %span
+ To register a new runner you should enter the following registration token.
+ With this token the runner will request a unique runner token and use that for future communication.
+ %br
+ Registration token is
+ %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
-.bs-callout
- %p
- A 'runner' is a process which runs a build.
- You can setup as many runners as you need.
- %br
- Runners can be placed on separate users, servers, and even on your local machine.
- %br
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ You can reset runners registration token by pressing a button below.
+ %p
+ = button_to reset_runners_token_admin_application_settings_path,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: 'Are you sure you want to reset registration token?' } do
+ = icon('refresh')
+ Reset runners registration token
+
+ .bs-callout
+ %p
+ A 'runner' is a process which runs a build.
+ You can setup as many runners as you need.
+ %br
+ Runners can be placed on separate users, servers, and even on your local machine.
+ %br
- %div
- %span Each runner can be in one of the following states:
- %ul
- %li
- %span.label.label-success shared
- \- run builds from all unassigned projects
- %li
- %span.label.label-info specific
- \- run builds from assigned projects
- %li
- %span.label.label-danger paused
- \- runner will not receive any new builds
+ %div
+ %span Each runner can be in one of the following states:
+ %ul
+ %li
+ %span.label.label-success shared
+ \- run builds from all unassigned projects
+ %li
+ %span.label.label-info specific
+ \- run builds from assigned projects
+ %li
+ %span.label.label-danger paused
+ \- runner will not receive any new builds
-.append-bottom-20.clearfix
- .pull-left
- = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
- .form-group
- = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
- = submit_tag 'Search', class: 'btn'
+ .append-bottom-20.clearfix
+ .pull-left
+ = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
+ = submit_tag 'Search', class: 'btn'
- .pull-right.light
- Runners with last contact less than a minute ago: #{@active_runners_cnt}
+ .pull-right.light
+ Runners with last contact less than a minute ago: #{@active_runners_cnt}
-%br
+ %br
-.table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Projects
- %th Builds
- %th Tags
- %th Last contact
- %th
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Projects
+ %th Builds
+ %th Tags
+ %th Last contact
+ %th
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
-= paginate @runners
+ - @runners.each do |runner|
+ = render "admin/runners/runner", runner: runner
+ = paginate @runners
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index e049b40bfab..61abfc6ecbe 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -28,7 +28,7 @@
.col-md-6
%h4 Restrict projects for this runner
- if @runner.projects.any?
- %table.table
+ %table.table.assigned-projects
%thead
%tr
%th Assigned projects
@@ -44,7 +44,7 @@
.pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
- %table.table
+ %table.table.unassigned-projects
%thead
%tr
%th Project
diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml
index dbecb7bbfd6..b0a709a568a 100644
--- a/app/views/admin/users/groups.html.haml
+++ b/app/views/admin/users/groups.html.haml
@@ -13,7 +13,7 @@
.pull-right
%span.light= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
- else
.nothing-here-block This user has no groups.
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/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index b655b2a15f5..84b9ceb23b3 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -38,6 +38,5 @@
%span.light= member.human_access
- if member.respond_to? :project
- = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times
-
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index d35f332e1e0..f7abad54286 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -13,7 +13,7 @@
Explore Projects
.nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
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/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
deleted file mode 100644
index 6bb542e658d..00000000000
--- a/app/views/groups/group_members/_group_member.html.haml
+++ /dev/null
@@ -1,57 +0,0 @@
-- user = member.user
-- return unless user || member.invite?
-- show_roles = local_assigns.fetch(:show_roles, true)
-
-%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
- %span{class: ("list-item-name" if show_controls)}
- - if member.user
- = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
- %strong
- = link_to user.name, user_path(user)
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
- - else
- = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
- %strong
- = member.invite_email
- %span.cgray
- invited
- - if member.created_by
- by
- = link_to member.created_by.name, user_path(member.created_by)
- = time_ago_with_tooltip(member.created_at)
-
- - if show_controls && can?(current_user, :admin_group_member, @group)
- = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
- Resend invite
-
- - if show_roles && should_user_see_group_roles?(current_user, @group)
- %span.pull-right
- %strong.member-access-level= member.human_access
- - if show_controls
- - if can?(current_user, :update_group_member, member)
- = button_tag class: "btn-xs btn btn-grouped inline js-toggle-button",
- title: 'Edit access level', type: 'button' do
- = icon('pencil')
-
- - if can?(current_user, :destroy_group_member, member)
- &nbsp;
- - if current_user == user
- = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- = icon("sign-out")
- Leave
- - else
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- = icon('trash')
-
- .edit-member.hide.js-toggle-content
- %br
- = form_for [@group, member], remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control'
- .prepend-top-10
- = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 0eb6bbd4420..d6acade84f1 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -6,18 +6,18 @@
.panel-heading
Add new user to group
.panel-body
- - if should_user_see_group_roles?(current_user, @group)
- %p.light
- Members of group have access to all group projects.
+ %p.light
+ Members of group have access to all group projects.
.new-group-member-holder
= render "new_group_member"
+ = render 'shared/members/requests', membership_source: @group, members: @members.request
+
.panel.panel-default
.panel-heading
%strong #{@group.name}
group members
- %small
- (#{@members.total_count})
+ %span.badge= @members.non_request.size
.controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
@@ -25,9 +25,8 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- - @members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: true
- = paginate @members, theme: 'gitlab'
+ = render partial: 'shared/members/member', collection: @members.non_request, as: :member
+ = paginate @members.non_request, theme: 'gitlab'
:javascript
$('form.member-search-form').on('submit', function(event) {
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index df726e2b2b9..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, show_controls: true))}');
+ $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 2b60a2efb11..5792c5dbac5 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -21,6 +21,9 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
+ - if current_user
+ = render 'shared/members/access_request_buttons', source: @group
+
%div{ class: container_class }
.top-area
%ul.nav-links
@@ -32,7 +35,7 @@
= link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects
.nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
= render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 01648047ce2..8cc0b59edeb 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -28,8 +28,12 @@
.key &#8984; shift p
- else
.key ctrl shift p
-
%td Toggle Markdown preview
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Edit last comment (when focused on an empty textarea)
%tbody
%tr
%th
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 6c4a9d68d1f..7486b1423e2 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -6,7 +6,7 @@
%p
%i.fa.fa-warning
- To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process.
+ To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository.
%p.light
Select projects you want to import.
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/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index 68a2d19e58d..96831874144 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -5,10 +5,28 @@ xml.entry do
xml.updated issue.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
- xml.author do |author|
+ xml.author do
xml.name issue.author_name
xml.email issue.author_email
end
xml.summary issue.title
+ xml.description issue.description if issue.description
+ xml.milestone issue.milestone.title if issue.milestone
+ xml.due_date issue.due_date if issue.due_date
+
+ unless issue.labels.empty?
+ xml.labels do
+ issue.labels.each do |label|
+ xml.label label.name
+ end
+ end
+ end
+
+ if issue.assignee
+ xml.assignee do
+ xml.name issue.assignee.name
+ xml.email issue.assignee.email
+ end
+ end
end
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..2234bf79c87 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 has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} 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 ad30a367fc5..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'}
@@ -27,9 +27,8 @@
%li
= link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw')
- - unless todos_pending_count == 0
- %span.badge.todos-pending-count
- = todos_pending_count
+ %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
+ = todos_pending_count
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
@@ -51,7 +50,7 @@
%h1.title= title
.header-logo
- #logo
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
= yield :header_content
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index f292730fe45..66e5ec1ad1a 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,107 +1,41 @@
-%ul.nav.nav-sidebar
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- = icon('dashboard fw')
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- = icon('cube fw')
- %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
- = 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
+%div{ class: nav_control_class }
+ = render 'layouts/nav/admin_settings'
- = 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
+ %ul.nav-links.scrolling-tabs
+ %li.fade-left
+ = icon('arrow-left')
+ = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
+ %span
+ Overview
+ = nav_link(controller: %w(background_jobs logs health_check)) do
+ = link_to admin_background_jobs_path, title: 'Monitoring' do
+ %span
+ Monitoring
+ = nav_link(controller: :broadcast_messages) do
+ = link_to admin_broadcast_messages_path, title: 'Messages' do
+ %span
+ Messages
+ = nav_link(controller: :hooks) do
+ = link_to admin_hooks_path, title: 'Hooks' do
+ %span
+ System Hooks
- = 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))
+ = nav_link(controller: :applications) do
+ = link_to admin_applications_path, title: 'Applications' do
+ %span
+ Applications
- - if askimet_enabled?
- = nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path, title: "Spam Logs" do
- = icon('exclamation-triangle fw')
+ = nav_link(controller: :abuse_reports) do
+ = link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
- Spam Logs
- %span.count= number_with_delimiter(SpamLog.count(:all))
+ Abuse Reports
+ %span.badge.count= number_with_delimiter(AbuseReport.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
+ - if askimet_enabled?
+ = nav_link(controller: :spam_logs) do
+ = link_to admin_spam_logs_path, title: "Spam Logs" do
+ %span
+ Spam Logs
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml
new file mode 100644
index 00000000000..38e9b80d129
--- /dev/null
+++ b/app/views/layouts/nav/_admin_settings.html.haml
@@ -0,0 +1,31 @@
+.controls
+ .dropdown.admin-settings-dropdown
+ %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ = nav_link(controller: :deploy_keys) do
+ = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
+ %span
+ Deploy Keys
+
+ = nav_link(controller: :services) do
+ = link_to admin_application_settings_services_path, title: 'Service Templates' do
+ %span
+ Service Templates
+
+ = nav_link(controller: :labels) do
+ = link_to admin_labels_path, title: 'Labels' do
+ %span
+ Labels
+
+ = nav_link(controller: :appearances) do
+ = link_to admin_appearances_path, title: 'Appearances' do
+ %span
+ Appearance
+
+ %li.divider
+ = nav_link(controller: :application_settings) do
+ = link_to admin_application_settings_path, title: 'Settings' do
+ %span
+ Settings
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 18cae5bf87f..52e41b1a857 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,54 +1,64 @@
%ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- = navbar_icon('project')
+ .icon-container
+ = navbar_icon('project')
%span
Projects
= nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do
- = icon('bell fw')
+ .icon-container
+ = icon('bell fw')
%span
Todos
%span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- = navbar_icon('activity')
+ .icon-container
+ = navbar_icon('activity')
%span
Activity
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
- = navbar_icon('group')
+ .icon-container
+ = navbar_icon('group')
%span
Groups
= nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
- = navbar_icon('milestones')
+ .icon-container
+ = navbar_icon('milestones')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- = navbar_icon('issues')
+ .icon-container
+ = navbar_icon('issues')
%span
Issues
%span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
- = navbar_icon('mr')
+ .icon-container
+ = navbar_icon('mr')
%span
Merge Requests
%span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
- = icon('clipboard fw')
+ .icon-container
+ = icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
- = icon('question-circle fw')
+ .icon-container
+ = icon('question-circle fw')
%span
Help
= nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
- = icon('user fw')
+ .icon-container
+ = icon('user fw')
%span
Profile Settings
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 66361a644dd..f7aa9fab7cf 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -2,7 +2,8 @@
= render 'layouts/nav/group_settings'
%ul.nav-links.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
%span
@@ -31,4 +32,5 @@
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 0b2673f1a82..3a24b09ab7e 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,20 +1,22 @@
- if current_user
- - if access = @group.users.find_by(id: current_user.id)
- .controls
- .dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - if can?(current_user, :admin_group, @group)
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects' do
- Projects
- %li.divider
- %li
- = link_to edit_group_path(@group) do
- Edit Group
+ - can_edit = can?(current_user, :admin_group, @group)
+ - member = @group.members.non_request.find_by(user_id: current_user.id)
+ - can_leave = member && can?(current_user, :destroy_group_member, member)
+
+ .controls
+ .dropdown.group-settings-dropdown
+ %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ = nav_link(path: 'groups#projects') do
+ = link_to 'Projects', projects_group_path(@group), title: 'Projects'
+ %li.divider
+ - if can_edit
%li
- = link_to leave_group_group_members_path(@group),
- data: { confirm: leave_group_message(@group.name) }, method: :delete, title: 'Leave group' do
+ = link_to 'Edit Group', edit_group_path(@group)
+ - if can_leave
+ %li
+ = link_to polymorphic_path([:leave, @group, :members]),
+ data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
Leave Group
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index d4b1f477f3f..44ea939b7e4 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,5 +1,6 @@
%ul.nav-links.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
%span
@@ -13,6 +14,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
@@ -39,4 +44,5 @@
= link_to audit_log_profile_path, title: 'Audit Log' do
%span
Audit Log
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 53d1fcc30a6..27e840df503 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,27 +1,33 @@
- if current_user
.controls
- - access = user_max_access_in_project(current_user.id, @project)
- - can_edit = can?(current_user, :admin_project, @project)
.dropdown.project-settings-dropdown
%a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- = render 'layouts/nav/project_settings'
- %li.divider
- - if can_edit
- %li
- = link_to edit_project_path(@project) do
- Edit Project
- - if access
- %li
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
- Leave Project
+ - can_edit = can?(current_user, :admin_project, @project)
+ -# We don't use @project.team.find_member because it searches for group members too...
+ - member = @project.members.non_request.find_by(user_id: current_user.id)
+ - can_leave = member && can?(current_user, :destroy_project_member, member)
+
+ = render 'layouts/nav/project_settings', can_edit: can_edit
+
+ - if can_edit || can_leave
+ %li.divider
+ - if can_edit
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
+ - if can_leave
+ %li
+ = link_to polymorphic_path([:leave, @project, :members]),
+ data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
+ Leave Project
%div{ class: nav_control_class }
%ul.nav-links.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
%span
@@ -34,12 +40,12 @@
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
- = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do
+ = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do
%span
- Code
+ Repository
- if project_nav_tab? :pipelines
- = nav_link(controller: :pipelines) do
+ = nav_link(controller: [:pipelines, :builds, :environments]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
@@ -105,4 +111,5 @@
%li.hidden
= link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
Commits
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 885e78d38c6..51a54b4f262 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -3,43 +3,43 @@
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
-
-- if @project.allowed_to_share_with_group?
- = nav_link(controller: :group_links) do
- = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
- %span
- Groups
-= nav_link(controller: :deploy_keys) do
- = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
- %span
- Deploy Keys
-= nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
- %span
- Webhooks
-= nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
- %span
- Services
-= nav_link(controller: :protected_branches) do
- = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
- %span
- Protected Branches
-
-- if @project.builds_enabled?
- = nav_link(controller: :runners) do
- = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
+- if can_edit
+ - if @project.allowed_to_share_with_group?
+ = nav_link(controller: :group_links) do
+ = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
+ %span
+ Groups
+ = nav_link(controller: :deploy_keys) do
+ = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
- Runners
- = nav_link(controller: :variables) do
- = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
+ Deploy Keys
+ = nav_link(controller: :hooks) do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
%span
- Variables
- = nav_link(controller: :triggers) do
- = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
+ Webhooks
+ = nav_link(controller: :services) do
+ = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
%span
- Triggers
- = nav_link(controller: :badges) do
- = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
+ Services
+ = nav_link(controller: :protected_branches) do
+ = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span
- Badges
+ Protected Branches
+
+ - if @project.builds_enabled?
+ = nav_link(controller: :runners) do
+ = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
+ %span
+ Runners
+ = nav_link(controller: :variables) do
+ = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
+ %span
+ Variables
+ = nav_link(controller: :triggers) do
+ = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
+ %span
+ Triggers
+ = nav_link(controller: :badges) do
+ = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
+ %span
+ Badges
diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml
deleted file mode 100644
index f1916d624b6..00000000000
--- a/app/views/notify/group_access_granted_email.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%p
- = "You have been granted #{@group_member.human_access} access to group"
- = link_to group_url(@group) do
- = @group.name
diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb
deleted file mode 100644
index ef9617bfc16..00000000000
--- a/app/views/notify/group_access_granted_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-
-You have been granted <%= @group_member.human_access %> access to group <%= @group.name %>
-
-<%= url_for(group_url(@group)) %>
diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml
deleted file mode 100644
index 55efad384a7..00000000000
--- a/app/views/notify/group_invite_accepted_email.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p
- #{@group_member.invite_email}, now known as
- #{link_to @group_member.user.name, user_url(@group_member.user)},
- has accepted your invitation to join group
- #{link_to @group.name, group_url(@group)}.
-
diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb
deleted file mode 100644
index f8b70f7a5a6..00000000000
--- a/app/views/notify/group_invite_accepted_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
-
-<%= group_url(@group) %>
diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml
deleted file mode 100644
index f9525d84fac..00000000000
--- a/app/views/notify/group_invite_declined_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
- #{@invite_email}
- has declined your invitation to join group
- #{link_to @group.name, group_url(@group)}.
-
diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb
deleted file mode 100644
index 6c19a288d15..00000000000
--- a/app/views/notify/group_invite_declined_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
-
-<%= group_url(@group) %>
diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml
deleted file mode 100644
index 163e88bfea3..00000000000
--- a/app/views/notify/group_member_invited_email.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%p
- You have been invited
- - if inviter = @group_member.created_by
- by
- = link_to inviter.name, user_url(inviter)
- to join group
- = link_to @group.name, group_url(@group)
- as #{@group_member.human_access}.
-
-%p
- = link_to 'Accept invitation', invite_url(@token)
- or
- = link_to 'decline', decline_invite_url(@token)
-
diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb
deleted file mode 100644
index 28ce4819b14..00000000000
--- a/app/views/notify/group_member_invited_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>.
-
-Accept invitation: <%= invite_url(@token) %>
-Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/member_access_denied_email.html.haml b/app/views/notify/member_access_denied_email.html.haml
new file mode 100644
index 00000000000..71c9c50071a
--- /dev/null
+++ b/app/views/notify/member_access_denied_email.html.haml
@@ -0,0 +1,4 @@
+%p
+ Your request to join the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}
+ has been denied.
diff --git a/app/views/notify/member_access_denied_email.text.erb b/app/views/notify/member_access_denied_email.text.erb
new file mode 100644
index 00000000000..87f2ef817ee
--- /dev/null
+++ b/app/views/notify/member_access_denied_email.text.erb
@@ -0,0 +1,3 @@
+Your request to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> has been denied.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
new file mode 100644
index 00000000000..18dec806539
--- /dev/null
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ You have been granted #{member.human_access} access to the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_access_granted_email.text.erb b/app/views/notify/member_access_granted_email.text.erb
new file mode 100644
index 00000000000..a9fb3a589a5
--- /dev/null
+++ b/app/views/notify/member_access_granted_email.text.erb
@@ -0,0 +1,3 @@
+You have been granted <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_access_requested_email.html.haml b/app/views/notify/member_access_requested_email.html.haml
new file mode 100644
index 00000000000..76f1f08a0cb
--- /dev/null
+++ b/app/views/notify/member_access_requested_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{link_to member.user.name, member.user} requested #{member.human_access}
+ access to the #{link_to member_source.human_name, polymorphic_url([member_source, :members])} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb
new file mode 100644
index 00000000000..9c5ee0eaf26
--- /dev/null
+++ b/app/views/notify/member_access_requested_email.text.erb
@@ -0,0 +1,3 @@
+<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= polymorphic_url([member_source, :members]) %>
diff --git a/app/views/notify/member_invite_accepted_email.html.haml b/app/views/notify/member_invite_accepted_email.html.haml
new file mode 100644
index 00000000000..2d1d40881eb
--- /dev/null
+++ b/app/views/notify/member_invite_accepted_email.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{member.invite_email}, now known as
+ #{link_to member.user.name, user_url(member.user)},
+ has accepted your invitation to join the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb
new file mode 100644
index 00000000000..cef87101427
--- /dev/null
+++ b/app/views/notify/member_invite_accepted_email.text.erb
@@ -0,0 +1,3 @@
+<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_invite_declined_email.html.haml b/app/views/notify/member_invite_declined_email.html.haml
new file mode 100644
index 00000000000..aa1b373d1a6
--- /dev/null
+++ b/app/views/notify/member_invite_declined_email.html.haml
@@ -0,0 +1,4 @@
+%p
+ #{@invite_email}
+ has declined your invitation to join the
+ #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
diff --git a/app/views/notify/member_invite_declined_email.text.erb b/app/views/notify/member_invite_declined_email.text.erb
new file mode 100644
index 00000000000..8bc305910c4
--- /dev/null
+++ b/app/views/notify/member_invite_declined_email.text.erb
@@ -0,0 +1,3 @@
+<%= @invite_email %> has declined your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+
+<%= member_source.web_url %>
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
new file mode 100644
index 00000000000..b8b75da3f2f
--- /dev/null
+++ b/app/views/notify/member_invited_email.html.haml
@@ -0,0 +1,13 @@
+%p
+ You have been invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_url(member.created_by)
+ to join the
+ = link_to member_source.human_name, member_source.web_url
+ #{member_source.model_name.singular} as #{member.human_access}.
+
+%p
+ = link_to 'Accept invitation', invite_url(@token)
+ or
+ = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb
new file mode 100644
index 00000000000..0a6393355be
--- /dev/null
+++ b/app/views/notify/member_invited_email.text.erb
@@ -0,0 +1,4 @@
+You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
+
+Accept invitation: <%= invite_url(@token) %>
+Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml
deleted file mode 100644
index dfc30a2d360..00000000000
--- a/app/views/notify/project_access_granted_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
- = "You have been granted #{@project_member.human_access} access to project"
-%p
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name_with_namespace
diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb
deleted file mode 100644
index 68eb1611ba7..00000000000
--- a/app/views/notify/project_access_granted_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-
-You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %>
-
-<%= url_for(namespace_project_url(@project.namespace, @project)) %>
diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml
deleted file mode 100644
index 7e58d30b10a..00000000000
--- a/app/views/notify/project_invite_accepted_email.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p
- #{@project_member.invite_email}, now known as
- #{link_to @project_member.user.name, user_url(@project_member.user)},
- has accepted your invitation to join project
- #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
-
diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb
deleted file mode 100644
index fcbe752114d..00000000000
--- a/app/views/notify/project_invite_accepted_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>.
-
-<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml
deleted file mode 100644
index c2d7e6f6e3a..00000000000
--- a/app/views/notify/project_invite_declined_email.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%p
- #{@invite_email}
- has declined your invitation to join project
- #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
-
diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb
deleted file mode 100644
index 484687fa51c..00000000000
--- a/app/views/notify/project_invite_declined_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
-
-<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml
deleted file mode 100644
index 79eb89616de..00000000000
--- a/app/views/notify/project_member_invited_email.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%p
- You have been invited
- - if inviter = @project_member.created_by
- by
- = link_to inviter.name, user_url(inviter)
- to join project
- = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)
- as #{@project_member.human_access}.
-
-%p
- = link_to 'Accept invitation', invite_url(@token)
- or
- = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb
deleted file mode 100644
index e0706272115..00000000000
--- a/app/views/notify/project_member_invited_email.text.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>.
-
-Accept invitation: <%= invite_url(@token) %>
-Decline invitation: <%= decline_invite_url(@token) %>
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..c888da29c17
--- /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.haml b/app/views/notify/project_was_not_exported_email.text.haml
new file mode 100644
index 00000000000..b27cb620b9e
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.text.haml
@@ -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/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 3d2a245ecbd..8efe486e01b 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -62,10 +62,14 @@
.provider-btn-image
= provider_image_tag(provider)
- if auth_active?(provider)
- = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
- Disconnect
+ - if provider.to_s == 'saml'
+ %a.provider-btn
+ Active
+ - else
+ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
+ Disconnect
- else
- = link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do
+ = link_to user_omniauth_authorize_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
Connect
%hr
- if current_user.can_change_username?
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 04becd5d860..537bba21f4a 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -9,4 +9,4 @@
= link_to group.name, group_path(group)
.pull-right
- = render 'notifications/buttons/notifications', notification_setting: setting
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index 664a17418db..5b2a69b8891 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -9,4 +9,4 @@
= link_to_project(project)
.pull-right
- = render 'notifications/buttons/notifications', notification_setting: setting
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index ef65ac53302..f77738f97f5 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -25,8 +25,11 @@
= f.label :notification_email, class: "label-light"
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
- .form-group.pull-left
- = render 'notifications/buttons/notifications', notification_setting: @global_notification_setting
+ = label_tag :global_notification_level, "Global notification level", class: "label-light"
+ %br
+ .clearfix
+ .form-group.pull-left.global-notification-setting
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
.clearfix
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/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index ce76cb73c9c..593be2617c1 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -51,9 +51,9 @@
%p
Use a hardware device to add the second factor of authentication.
%p
- As U2F devices are only supported by a few browsers, it's recommended that you set up a
- two-factor authentication app as well as a U2F device so you'll always be able to log in
- using an unsupported browser.
+ As U2F devices are only supported by a few browsers, we require that you set up a
+ two-factor authentication app before a U2F device. That way you'll always be able to
+ log in - even when you're using an unsupported browser.
.col-lg-9
%p
- if @registration_key_handles.present?
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index df725d81a39..86ea08dd229 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -21,7 +21,7 @@
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- .project-repo-buttons
+ .project-repo-buttons.project-action-buttons
.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
@@ -30,9 +30,12 @@
= render "shared/clone_panel"
.project-repo-buttons.btn-group.project-right-buttons
+ - if current_user
+ = render 'shared/members/access_request_buttons', source: @project
+
= render "projects/buttons/download"
= render 'projects/buttons/dropdown'
- = render 'notifications/buttons/notifications', notification_setting: @notification_setting
+ = render 'shared/notifications/button', notification_setting: @notification_setting
:javascript
new Star();
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/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 28a28282fd3..ca6714ef42b 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -14,8 +14,17 @@
%span This is a confidential issue. Your comment will not be visible to the public.
%li.pull-right
- %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 }
- Go full screen
+ .toolbar-group
+ = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`" }, title: "Insert code" })
+ = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ .toolbar-group
+ %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
+ =icon("arrows-alt fw")
.md-write-holder
= yield
@@ -24,7 +33,7 @@
- if defined?(referenced_users) && referenced_users
%div.referenced-users.hide
%span
- = icon('exclamation-triangle')
+ = icon("exclamation-triangle")
You are about to add
%strong
%span.js-referenced-users-count 0
diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml
index ee63bc55a30..ac80951dd4f 100644
--- a/app/views/projects/badges/index.html.haml
+++ b/app/views/projects/badges/index.html.haml
@@ -7,7 +7,7 @@
%b Builds badge &middot;
= @build_badge.to_html
.pull-right
- = render 'shared/ref_switcher', destination: 'badges'
+ = render 'shared/ref_switcher', destination: 'badges', align_right: true
.panel-body
.row
.col-md-2.text-center
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 4071b59c003..29c7d45074a 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -13,12 +13,12 @@
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 } } )
+ .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
+ = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 87c732626a6..4bd85061240 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -20,15 +20,15 @@
protected
.controls.hidden-xs
- if create_mr_button?(@repository.root_ref, branch.name)
- = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do
+ = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
Merge Request
- if branch.name != @repository.root_ref
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
Compare
- if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 5d931389dfb..cab21f0cf19 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -11,19 +11,33 @@
%p.build-detail-row
#{@build.coverage}%
- - if can?(current_user, :read_build, @project) && @build.artifacts?
+ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
.title
Build artifacts
- .btn-group.btn-group-justified{ role: :group }
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
- Download
+ - if @build.artifacts_expired?
+ %p.build-detail-row
+ The artifacts were removed
+ #{time_ago_with_tooltip(@build.artifacts_expire_at)}
+ - elsif @build.artifacts_expire_at
+ %p.build-detail-row
+ The artifacts will be removed in
+ %span.js-artifacts-remove= @build.artifacts_expire_at
- - if @build.artifacts_metadata?
- = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
- Browse
+ - if @build.artifacts?
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.artifacts_expire_at
+ = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
+ Keep
- .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && @build.artifacts?)) }
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Download
+
+ - if @build.artifacts_metadata?
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Browse
+
+ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title
Build details
- if @build.retryable?
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/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 02dbb2985a4..71cf5582a4c 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,5 +1,5 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: "Star project" do
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
- if current_user.starred?(@project)
= icon('star fw')
%span.starred Unstar
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index a0ffa065067..e38d1ff5ff0 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -24,8 +24,8 @@
%span.label.label-warning stuck
%p.commit-title
- - if commit_data = pipeline.commit_data
- = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message"
+ - if commit = pipeline.commit
+ = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
@@ -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/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index b117517c0dd..3ad866bb2f1 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -6,10 +6,10 @@
.pull-right.commit-action-buttons
- if defined?(@notes_count) && @notes_count > 0
- %span.btn.disabled.btn-grouped.hidden-xs
+ %span.btn.disabled.btn-grouped.hidden-xs.append-right-10
= icon('comment')
= @notes_count
- = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped hidden-xs hidden-sm" do
+ = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do
Browse Files
.dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 367027182b6..929496f81d8 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,26 +9,31 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .commit-row-title
- %span.item-title
- = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- - if commit.description?
- %a.text-expander.js-toggle-button ...
+ = commit_author_avatar(commit, size: 36)
+ .commit-info-block
+ .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.hidden-xs.js-toggle-button ...
- .pull-right
- - 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"
+ .commit-actions.hidden-xs
+ - if commit.status
+ = 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
+ - if commit.description?
+ %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-row-info
+ = 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/_head.html.haml b/app/views/projects/commits/_head.html.haml
index a72e8ba73ad..54dab4bff07 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,7 +1,8 @@
.scrolling-tabs-container
- %ul.nav-links.sub-nav.scrolling-tabs
- %div{ class: (container_class) }
- .fade-left
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_files_path(@project) do
Files
@@ -25,4 +26,5 @@
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
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/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index c322942aeba..b22285c11e0 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -3,7 +3,7 @@
= render "projects/commits/head"
%div{ class: (container_class) }
- .row-content-block.second-block.content-component-block
+ .sub-header-block
Compare branches, tags or commit ranges.
%br
Fill input field with commit id like
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index cdc34f51d6d..f4ec7b767f6 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,24 +1,24 @@
+- @no_container = true
- page_title "#{params[:from]}...#{params[:to]}"
= render "projects/commits/head"
+%div{ class: (container_class) }
+ .sub-header-block.no-bottom-space
+ = render "form"
-.row-content-block
- = render "form"
-
-- if @commits.present?
- .prepend-top-default
+ - if @commits.present?
= render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs
-- else
- .light-well.prepend-top-default
- .center
- %h4
- There isn't anything to compare.
- %p.slead
- - if params[:to] == params[:from]
- %span.label-branch #{params[:from]}
- and
- %span.label-branch #{params[:to]}
- are the same.
- - else
- You'll need to use different branch names to get a valid comparison.
+ - else
+ .light-well
+ .center
+ %h4
+ There isn't anything to compare.
+ %p.slead
+ - if params[:to] == params[:from]
+ %span.label-branch #{params[:from]}
+ and
+ %span.label-branch #{params[:to]}
+ are the same.
+ - else
+ You'll need to use different branch names to get a valid comparison.
diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml
index 4e9f936539b..10822b6184c 100644
--- a/app/views/projects/container_registry/_tag.html.haml
+++ b/app/views/projects/container_registry/_tag.html.haml
@@ -3,17 +3,25 @@
= escape_once(tag.name)
= clipboard_button(clipboard_text: "docker pull #{tag.path}")
%td
- - if layer = tag.layers.first
- %span.has-tooltip{ title: "#{layer.revision}" }
- = layer.short_revision
+ - if tag.revision
+ %span.has-tooltip{ title: "#{tag.revision}" }
+ = tag.short_revision
- else
\-
%td
- = number_to_human_size(tag.total_size)
- &middot;
- = pluralize(tag.layers.size, "layer")
+ - if tag.total_size
+ = number_to_human_size(tag.total_size)
+ &middot;
+ = pluralize(tag.layers.size, "layer")
+ - else
+ .light
+ \-
%td
- = time_ago_in_words(tag.created_at)
+ - if tag.created_at
+ = time_ago_in_words(tag.created_at)
+ - else
+ .light
+ \-
- if can?(current_user, :update_container_image, @project)
%td.content
.controls.hidden-xs.pull-right
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
new file mode 100644
index 00000000000..0f9d9512d88
--- /dev/null
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -0,0 +1,12 @@
+%div.branch-commit
+ - if deployment.ref
+ = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace"
+ &middot;
+ = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
+
+ %p.commit-title
+ %span
+ - if commit_title = deployment.commit_title
+ = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message"
+ - else
+ Cant find HEAD commit for this branch
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
new file mode 100644
index 00000000000..d08dd92f1f6
--- /dev/null
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -0,0 +1,23 @@
+%tr.deployment
+ %td
+ %strong= "##{deployment.iid}"
+
+ %td
+ = render 'projects/deployments/commit', deployment: deployment
+
+ %td
+ - if deployment.deployable
+ = link_to namespace_project_build_path(@project.namespace, @project, deployment.deployable) do
+ = "#{deployment.deployable.name} (##{deployment.deployable.id})"
+
+ %td
+ #{time_ago_with_tooltip(deployment.created_at)}
+
+ %td
+ - if can?(current_user, :create_deployment, deployment) && deployment.deployable
+ .pull-right
+ = link_to retry_namespace_project_build_path(@project.namespace, @project, deployment.deployable), method: :post, class: 'btn btn-build' do
+ - if deployment.last?
+ Retry
+ - else
+ Rollback
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index d9c4b410d32..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
@@ -24,6 +26,7 @@
- diff_commit = commit_for_diff(diff_file)
- blob = project.repository.blob_for_diff(diff_commit, diff_file)
- next unless blob
+ - blob.load_all_data!(project.repository) unless blob.only_display_raw?
= render 'projects/diffs/file', i: index, project: project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index e5983c58039..2395ea3c275 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -49,6 +49,8 @@
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- else
= render "projects/diffs/text_file", diff_file: diff_file, index: i
+ - elsif blob.only_display_raw?
+ .nothing-here-block This file is too large to display.
- elsif blob.image?
- old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs
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/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
new file mode 100644
index 00000000000..eafa246d05f
--- /dev/null
+++ b/app/views/projects/environments/_environment.html.haml
@@ -0,0 +1,17 @@
+- last_deployment = environment.last_deployment
+
+%tr.environment
+ %td
+ %strong
+ = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
+
+ %td
+ - if last_deployment
+ = render 'projects/deployments/commit', deployment: last_deployment
+ - else
+ %p.commit-title
+ No deployments yet
+
+ %td
+ - if last_deployment
+ #{time_ago_with_tooltip(last_deployment.created_at)}
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
new file mode 100644
index 00000000000..c07f4bd510c
--- /dev/null
+++ b/app/views/projects/environments/_form.html.haml
@@ -0,0 +1,7 @@
+= form_for @environment, url: namespace_project_environments_path(@project.namespace, @project), html: { class: 'col-lg-9' } do |f|
+ = form_errors(@environment)
+ .form-group
+ = f.label :name, 'Name', class: 'label-light'
+ = f.text_field :name, required: true, class: 'form-control'
+ = f.submit 'Create environment', class: 'btn btn-create'
+ = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel'
diff --git a/app/views/projects/environments/_header_title.html.haml b/app/views/projects/environments/_header_title.html.haml
new file mode 100644
index 00000000000..e056fccad5d
--- /dev/null
+++ b/app/views/projects/environments/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Environments", project_environments_path(@project))
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
new file mode 100644
index 00000000000..a03f117291f
--- /dev/null
+++ b/app/views/projects/environments/index.html.haml
@@ -0,0 +1,31 @@
+- @no_container = true
+- page_title "Environments"
+= render "projects/pipelines/head"
+
+%div{ class: (container_class) }
+ - if can?(current_user, :create_environment, @project) && !@environments.blank?
+ .top-area
+ .nav-controls
+ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+ New environment
+
+ - if @environments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any environments right now.
+ %p.blank-state-text
+ Environments are places where code gets deployed, such as staging or production.
+ %br
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci", "environments")
+ - if can?(current_user, :create_environment, @project)
+ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+ New environment
+ - else
+ .table-holder
+ %table.table.environments
+ %tbody
+ %th Environment
+ %th Last deployment
+ %th Date
+ = render @environments
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
new file mode 100644
index 00000000000..da325efecd2
--- /dev/null
+++ b/app/views/projects/environments/new.html.haml
@@ -0,0 +1,12 @@
+- page_title 'New Environment'
+
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ New Environment
+ %p
+ Environments allow you to track deployments of your application
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci", "environments")
+
+ = render 'form'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
new file mode 100644
index 00000000000..4c15e2759d6
--- /dev/null
+++ b/app/views/projects/environments/show.html.haml
@@ -0,0 +1,37 @@
+- @no_container = true
+- page_title "Environments"
+= render "projects/pipelines/head"
+
+%div{ class: (container_class) }
+ .top-area
+ .col-md-9
+ %h3.page-title= @environment.name.titleize
+
+ .col-md-3
+ .nav-controls
+ - if can?(current_user, :update_environment, @environment)
+ = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
+
+ - if @deployments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci", "environments"), class: "btn btn-success"
+ - else
+ .table-holder
+ %table.table.environments
+ %thead
+ %tr
+ %th ID
+ %th Commit
+ %th Build
+ %th Date
+ %th
+
+ = render @deployments
+
+ = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 4bcf2d9d533..dbe9ddfde2f 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -40,9 +40,3 @@
= render 'projects', projects: @forks
-
-- if @private_forks_count > 0
- .private-forks-notice
- = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
- %strong= pluralize(@private_forks_count, 'private fork')
- %span you have no access to.
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 8becaea246f..a388d9a0a61 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,12 +1,14 @@
-- page_specific_javascripts asset_path("graphs/application.js")
-%ul.nav-links
- = nav_link(action: :show) do
- = link_to 'Contributors', namespace_project_graph_path
- = nav_link(action: :commits) do
- = link_to 'Commits', commits_namespace_project_graph_path
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.builds_enabled?
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+
+ - page_specific_javascripts asset_path("graphs/application.js")
+ = nav_link(action: :show) do
+ = link_to 'Contributors', namespace_project_graph_path
+ = nav_link(action: :commits) do
+ = link_to 'Commits', commits_namespace_project_graph_path
+ = nav_link(action: :languages) do
+ = link_to 'Languages', languages_namespace_project_graph_path
+ - if @project.builds_enabled?
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
index 19ccc125ea8..e695d3ae369 100644
--- a/app/views/projects/graphs/ci.html.haml
+++ b/app/views/projects/graphs/ci.html.haml
@@ -1,15 +1,18 @@
+- @no_container = true
- page_title "Continuous Integration", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .oneline
- A collection of graphs for Continuous Integration
-#charts.ci-charts
- .row
- .col-md-6
- = render 'projects/graphs/ci/overall'
- .col-md-6
- = render 'projects/graphs/ci/build_times'
+%div{ class: (container_class) }
+ .sub-header-block
+ .oneline
+ A collection of graphs for Continuous Integration
- %hr
- = render 'projects/graphs/ci/builds'
+ #charts.ci-charts
+ .row
+ .col-md-6
+ = render 'projects/graphs/ci/overall'
+ .col-md-6
+ = render 'projects/graphs/ci/build_times'
+
+ %hr
+ = render 'projects/graphs/ci/builds'
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index d9b2fb6c065..0daffe68f6f 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,52 +1,54 @@
+- @no_container = true
- page_title "Commits", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs_commits'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+%div{ class: (container_class) }
+ .sub-header-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs_commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
-%p.lead
- Commit statistics for
- %strong #{@ref}
- #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
+ %p.lead
+ Commit statistics for
+ %strong #{@ref}
+ #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
-.row
- .col-md-6
- %ul
- %li
- %p.lead
- %strong #{@commits_graph.commits.size}
- commits during
- %strong #{@commits_graph.duration}
- days
- %li
- %p.lead
- Average
- %strong #{@commits_graph.commit_per_day}
- commits per day
- %li
- %p.lead
- Contributed by
- %strong #{@commits_graph.authors}
- authors
- .col-md-6
- %div
- %p.slead
- Commits per day of month
- %canvas#month-chart
-.row
- .col-md-6
- %div
- %p.slead
- Commits per day hour (UTC)
- %canvas#hour-chart
- .col-md-6
- %div
- %p.slead
- Commits per weekday
- %canvas#weekday-chart
+ .row
+ .col-md-6
+ %ul
+ %li
+ %p.lead
+ %strong #{@commits_graph.commits.size}
+ commits during
+ %strong #{@commits_graph.duration}
+ days
+ %li
+ %p.lead
+ Average
+ %strong #{@commits_graph.commit_per_day}
+ commits per day
+ %li
+ %p.lead
+ Contributed by
+ %strong #{@commits_graph.authors}
+ authors
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day of month
+ %canvas#month-chart
+ .row
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day hour (UTC)
+ %canvas#hour-chart
+ .col-md-6
+ %div
+ %p.slead
+ Commits per weekday
+ %canvas#weekday-chart
:javascript
var responsiveChart = function (selector, data) {
diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml
index 249c16f4709..6d97f552a8e 100644
--- a/app/views/projects/graphs/languages.html.haml
+++ b/app/views/projects/graphs/languages.html.haml
@@ -1,24 +1,26 @@
+- @no_container = true
- page_title "Languages", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .oneline
- Programming languages used in this repository
+%div{ class: (container_class) }
+ .sub-header-block
+ .oneline
+ Programming languages used in this repository
-.row
- .col-md-8
- %canvas#languages-chart{ height: 400 }
- .col-md-4
- %ul.bordered-list
- - @languages.each do |language|
- %li
- %span{ style: "color: #{language[:color]}" }
- = icon('circle')
- &nbsp;
- = language[:label]
- .pull-right
- = language[:value]
- \%
+ .row
+ .col-md-8
+ %canvas#languages-chart{ height: 400 }
+ .col-md-4
+ %ul.bordered-list
+ - @languages.each do |language|
+ %li
+ %span{ style: "color: #{language[:color]}" }
+ = icon('circle')
+ &nbsp;
+ = language[:label]
+ .pull-right
+ = language[:value]
+ \%
:javascript
var data = #{@languages.to_json};
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 33970e7b909..9f7e2a361ff 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,29 +1,31 @@
+- @no_container = true
- page_title "Contributors", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
-
-.loading-graph
- .center
- %h3.page-title
- %i.fa.fa-spinner.fa-spin
- Building repository graph.
- %p.slead Please wait a moment, this page will automatically refresh when ready.
-
-.stat-graph.hide
- .header.clearfix
- %h3#date_header.page-title
- %p.light
- Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
- %input#brush_change{:type => "hidden"}
- .graphs
- #contributors-master
- #contributors.clearfix
- %ol.contributors-list.clearfix
+%div{ class: (container_class) }
+ .sub-header-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
+ .loading-graph
+ .center
+ %h3.page-title
+ %i.fa.fa-spinner.fa-spin
+ Building repository graph.
+ %p.slead Please wait a moment, this page will automatically refresh when ready.
+
+ .stat-graph.hide
+ .header.clearfix
+ %h3#date_header.page-title
+ %p.light
+ Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
+ %input#brush_change{:type => "hidden"}
+ .graphs.row
+ #contributors-master
+ #contributors.clearfix
+ %ol.contributors-list.clearfix
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/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index b151393abab..c2f4457b60b 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 166dae248b6..403adb7426b 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -1,5 +1,5 @@
-%ul.nav-links.sub-nav
- %div{ class: (container_class) }
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
- if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
= nav_link(controller: :issues) do
= link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do
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/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 393998f15b9..53dd300c35c 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,8 +1,8 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
+ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.closed?
- = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
+ = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
#notes= render "projects/notes/notes_with_form"
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/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index ec4beae9727..19b5d0ff066 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -6,46 +6,29 @@
- if @merge_request.merge_event
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
- %div
- - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
+ - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
+ %p
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ The source branch has been removed.
+ = render 'projects/merge_requests/widget/merged_buttons'
+ - elsif @merge_request.can_remove_source_branch?(current_user)
+ .remove_source_branch_widget
%p
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- The source branch has been removed.
- = render 'projects/merge_requests/widget/merged_buttons'
- - elsif @merge_request.can_remove_source_branch?(current_user)
- .remove_source_branch_widget
- %p
- The changes were merged into
- #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- You can remove the source branch now.
- = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
- .remove_source_branch_widget.failed.hide
- %p
- Failed to remove source branch '#{@merge_request.source_branch}'.
-
- .remove_source_branch_in_progress.hide
- %p
- = icon('spinner spin')
- Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded.
-
- :javascript
- $('.remove_source_branch').on('click', function() {
- $('.remove_source_branch_widget').hide();
- $('.remove_source_branch_in_progress').show();
- });
-
- $(".remove_source_branch").on("ajax:success", function (e, data, status, xhr) {
- location.reload();
- });
+ You can remove the source branch now.
+ = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
+ .remove_source_branch_widget.failed.hide
+ %p
+ Failed to remove source branch '#{@merge_request.source_branch}'.
- $(".remove_source_branch").on("ajax:error", function (e, data, status, xhr) {
- $('.remove_source_branch_widget').hide();
- $('.remove_source_branch_in_progress').hide();
- $('.remove_source_branch_widget.failed').show();
- });
- - else
+ .remove_source_branch_in_progress.hide
%p
- The changes were merged into
- #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
- = render 'projects/merge_requests/widget/merged_buttons'
+ = icon('spinner spin')
+ Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded.
+ - else
+ %p
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ = render 'projects/merge_requests/widget/merged_buttons'
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index 56167509af9..d836a253507 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -3,9 +3,9 @@
- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked?
- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
- .btn-group
+ .clearfix.merged-buttons
- if can_remove_source_branch
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
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..593af319a47 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: @url, commit_url: @commit_url, ref: @ref, commit_id: @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/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index c87a3fadf72..8620f492282 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -6,6 +6,6 @@
= render 'projects/notes/hints'
.note-form-actions.clearfix
- = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
+ = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 67ed38a7b22..03b3f6935d1 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -14,7 +14,7 @@
.error-alert
.note-form-actions.clearfix
- = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
+ = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button"
= yield(:note_actions)
%a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
Discard draft
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 0b002043408..7d1cbc62e86 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -5,4 +5,4 @@
is supported
%button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file
+ Attach a file \ No newline at end of file
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index bcdbff08011..c04d291412c 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -18,9 +18,9 @@
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-actions
- access = note.project.team.human_max_access(note.author.id)
- - if access
+ - if access and not note.system
%span.note-role.hidden-xs= access
- - if current_user
+ - if current_user and not note.system
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
= icon('smile-o')
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index d0ba0d27d7c..d65faf86d4e 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,5 +1,5 @@
-%ul.nav-links.sub-nav
- %div{ class: (container_class) }
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
- if project_nav_tab? :pipelines
= nav_link(controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
@@ -11,3 +11,9 @@
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
%span
Builds
+
+ - if project_nav_tab? :environments
+ = nav_link(controller: %w(environments)) do
+ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ %span
+ Environments
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 6671ee2c6d6..e783d8c72c5 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -2,15 +2,17 @@
.panel-heading
%strong #{@group.name}
group members
- %small
- (#{members.count})
+ %span.badge= members.size
- if can?(current_user, :admin_group_member, @group)
.controls
- = link_to group_group_members_path(@group), class: 'btn' do
- Manage group members
+ = link_to 'Manage group members',
+ group_group_members_path(@group),
+ class: 'btn'
%ul.content-list
- - members.limit(20).each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false
- - if members.count > 20
+ = render partial: 'shared/members/member',
+ collection: members.limit(20),
+ as: :member,
+ locals: { show_controls: false }
+ - if members.size > 20
%li
and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index f0f3bb3c177..82892a33358 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -9,7 +9,7 @@
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
.col-sm-10
- = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2"
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
deleted file mode 100644
index 268f140d7db..00000000000
--- a/app/views/projects/project_members/_project_member.html.haml
+++ /dev/null
@@ -1,55 +0,0 @@
-- user = member.user
-- return unless user || member.invite?
-
-%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
- %span.list-item-name
- - if member.user
- = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
- %strong
- = link_to user.name, user_path(user)
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
- - else
- = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
- %strong
- = member.invite_email
- %span.cgray
- invited
- - if member.created_by
- by
- = link_to member.created_by.name, user_path(member.created_by)
- = time_ago_with_tooltip(member.created_at)
-
- - if can?(current_user, :admin_project_member, @project)
- = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
- Resend invite
-
- - if can?(current_user, :admin_project_member, @project)
- .pull-right
- %strong= member.human_access
- - if can?(current_user, :update_project_member, member)
- = button_tag class: "btn-xs btn-grouped inline btn js-toggle-button",
- title: 'Edit access level', type: 'button' do
- = icon('pencil')
-
- - if can?(current_user, :destroy_project_member, member)
- &nbsp;
- - if current_user == user
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
- = icon("sign-out")
- Leave
- - else
- = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
- = icon('trash')
-
- .edit-member.hide.js-toggle-content
- %br
- = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
- .prepend-top-10
- = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
index ae13f8428f0..840b57c2e63 100644
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -1,6 +1,7 @@
- @project_group_links.each do |group_links|
- shared_group = group_links.group
- - shared_group_users_count = group_links.group.group_members.count
+ - shared_group_members = shared_group.members.non_request
+ - shared_group_users_count = shared_group_members.size
.panel.panel-default
.panel-heading
Shared with
@@ -14,8 +15,10 @@
%i.fa.fa-pencil-square-o
Edit group members
%ul.content-list
- - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+ = render partial: 'shared/members/member',
+ collection: shared_group_members.order(access_level: :desc).limit(20),
+ as: :member,
+ locals: { show_controls: false, show_roles: false }
- if shared_group_users_count > 20
%li
and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index e8dce30425f..b0bfdd235f7 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -2,8 +2,7 @@
.panel-heading
%strong #{@project.name}
project members
- %small
- (#{members.count})
+ %span.badge= members.size
.controls
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
@@ -11,8 +10,7 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- - members.each do |project_member|
- = render 'project_member', member: project_member
+ = render partial: 'shared/members/member', collection: members, as: :member
:javascript
$('form.member-search-form').on('submit', function (event) {
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 15dc064e7ea..a2026c41d01 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -13,10 +13,12 @@
Users with access to this project are listed below.
= render "new_project_member"
- = render "team", members: @project_members
+ = render 'shared/members/requests', membership_source: @project, members: @project_members.request
+
+ = render 'team', members: @project_members.non_request
- if @group
- = render "group_members", members: @group_members
+ = render "group_members", members: @group_members.non_request
- if @project_group_links.any? && @project.allowed_to_share_with_group?
= render "shared_group_members"
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/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index d62f5c8f131..c45a9d4f81f 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -13,6 +13,12 @@
= f.check_box :run_untagged
%span.light Indicates whether this runner can pick jobs without tags
.form-group
+ = label :locked, 'Lock to current projects', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :locked
+ %span.light When a runner is locked, it cannot be assigned to other projects
+ .form-group
= label_tag :token, class: 'control-label' do
Token
.col-sm-10
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 96e2aac451f..85225857758 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -2,8 +2,10 @@
%h4
= runner_status_icon(runner)
%span.monospace
- - if @runners.include?(runner)
+ - if @project_runners.include?(runner)
= link_to runner.short_sha, runner_path(runner)
+ - if runner.locked?
+ = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
%small
= link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
%i.fa.fa-edit.btn
@@ -11,7 +13,7 @@
= runner.short_sha
.pull-right
- - if @runners.include?(runner)
+ - if @project_runners.include?(runner)
- if runner.belongs_to_one_project?
= link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- else
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 8ae9f0d95f7..d469dda5b81 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -17,13 +17,13 @@
Start runner!
-- if @runners.any?
+- if @project_runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render partial: 'runner', collection: @runners, as: :runner
+ = render partial: 'runner', collection: @project_runners, as: :runner
-- if @specific_runners.any?
+- if @assignable_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render partial: 'runner', collection: @specific_runners, as: :runner
- = paginate @specific_runners
+ = render partial: 'runner', collection: @assignable_runners, as: :runner
+ = paginate @assignable_runners
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index f24e1b9144e..61b99f35d74 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -23,6 +23,9 @@
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
%tr
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
+ %tr
%td Tags
%td
- @runner.tag_list.each do |tag|
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4afa902b4eb..15f0d85194b 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
@@ -57,6 +57,10 @@
%li.missing
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide
+ - unless @repository.gitlab_ci_yml
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
+ Set Up CI
- if @repository.commit
.content-block.second-block.white
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 844e1055810..2c11c0e5b21 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -15,7 +15,7 @@
= render 'projects/tags/download', ref: tag.name, project: @project
- if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes" do
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
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/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 4faa547769b..4ea75dbbf0c 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,4 +1,7 @@
- if (@page && @page.persisted?)
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page History
- if can?(current_user, :create_wiki, @project)
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 988fe024e28..f8ea479e0b1 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,5 +1,5 @@
-.top-area
- %ul.nav-links
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
@@ -10,9 +10,4 @@
= link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
Git Access
- .nav-controls
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- New Page
-
-= render 'projects/wikis/new'
+ = render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 919daf0a7b2..4f8abcdc8e1 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -1,14 +1,17 @@
-%div#modal-new-wiki.modal
- .modal-dialog
- .modal-content
- .modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title New Wiki Page
- .modal-body
- %form.new-wiki-page
- .form-group
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
- .form-actions
- = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
+- @no_container = true
+
+%div{ class: (container_class) }
+ %div#modal-new-wiki.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3.page-title New Wiki Page
+ .modal-body
+ %form.new-wiki-page
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
+ .form-actions
+ = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index cbd69ee1a73..bf5d09d50c2 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,19 +1,24 @@
+- @no_container = true
- page_title "Edit", @page.title.capitalize, "Wiki"
= render 'nav'
-.top-area
- .nav-text.wiki-page
- %strong
- - if @page.persisted?
- = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- - else
- = @page.title.capitalize
- %span.light
- &middot;
- Edit Page
+%div{ class: (container_class) }
+ .top-area
+ .nav-text
+ %strong
+ - if @page.persisted?
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title.capitalize
+ %span.light
+ &middot;
+ Edit Page
- .nav-controls
- = render 'main_links'
+ .nav-controls
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
+ = render 'main_links'
-= render 'form'
+ = render 'form'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index ccceab6155e..6caf7230f35 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,32 +1,34 @@
+- @no_container = true
- page_title "Git Access", "Wiki"
= render 'nav'
-.row-content-block
- %span.oneline
- Git access for
- %strong= @project_wiki.path_with_namespace
+%div{ class: (container_class) }
+ .sub-header-block
+ %span.oneline
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .pull-right
- = render "shared/clone_panel", project: @project_wiki
+ .pull-right
+ = render "shared/clone_panel", project: @project_wiki
-.git-empty.prepend-top-default
- %fieldset
- %legend Install Gollum:
- %pre.dark
- :preserve
- gem install gollum
+ .prepend-top-default
+ %fieldset
+ %legend Install Gollum:
+ %pre.dark
+ :preserve
+ gem install gollum
- %legend Clone Your Wiki:
- %pre.dark
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{h @project_wiki.path}
+ %legend Clone Your Wiki:
+ %pre.dark
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ cd #{h @project_wiki.path}
- %legend Start Gollum And Edit Locally:
- %pre.dark
- :preserve
- gollum
- == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
- >> Thin web server (v1.5.0 codename Knife)
- >> Maximum connections set to 1024
- >> Listening on 0.0.0.0:4567, CTRL+C to stop
+ %legend Start Gollum And Edit Locally:
+ %pre.dark
+ :preserve
+ gollum
+ == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
+ >> Thin web server (v1.5.0 codename Knife)
+ >> Maximum connections set to 1024
+ >> Listening on 0.0.0.0:4567, CTRL+C to stop
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 45460ed9f41..630ee35b70b 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,37 +1,37 @@
- page_title "History", @page.title.capitalize, "Wiki"
= render 'nav'
+%div{ class: (container_class) }
+ .top-area
+ .nav-text
+ %strong
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ %span.light
+ &middot;
+ History
-.top-area
- .nav-text
- %strong
- = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- %span.light
- &middot;
- History
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Page version
- %th Author
- %th Commit Message
- %th Last updated
- %th Format
- %tbody
- - @page.versions.each_with_index do |version, index|
- - commit = version
+ .table-holder
+ %table.table
+ %thead
%tr
- %td
- = link_to project_wiki_path_with_version(@project, @page,
- commit.id, index == 0) do
- = truncate_sha(commit.id)
- %td
- = commit.author.name
- %td
- = commit.message
- %td
- #{time_ago_with_tooltip(version.authored_date)}
- %td
- %strong
- = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+ %th Page version
+ %th Author
+ %th Commit Message
+ %th Last updated
+ %th Format
+ %tbody
+ - @page.versions.each_with_index do |version, index|
+ - commit = version
+ %tr
+ %td
+ = link_to project_wiki_path_with_version(@project, @page,
+ commit.id, index == 0) do
+ = truncate_sha(commit.id)
+ %td
+ = commit.author.name
+ %td
+ = commit.message
+ %td
+ #{time_ago_with_tooltip(version.authored_date)}
+ %td
+ %strong
+ = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 2f6162fa3c5..81d9f391c1c 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,12 +1,14 @@
+- @no_container = true
- page_title "Pages", "Wiki"
= render 'nav'
-%ul.content-list
- - @wiki_pages.each do |wiki_page|
- %li
- = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
- %small (#{wiki_page.format})
- .pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
-= paginate @wiki_pages, theme: 'gitlab'
+%div{ class: (container_class) }
+ %ul.content-list
+ - @wiki_pages.each do |wiki_page|
+ %li
+ = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
+ %small (#{wiki_page.format})
+ .pull-right
+ %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ = paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 9166c0edb3b..76f9b1ecd76 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,24 +1,26 @@
+- @no_container = true
- page_title @page.title.capitalize, "Wiki"
= render 'nav'
-.top-area
- .nav-text
- %strong= @page.title.capitalize
+%div{ class: (container_class) }
+ .top-area
+ .nav-text
+ %strong= @page.title.capitalize
- %span.wiki-last-edit-by
- &middot;
- last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ %span.wiki-last-edit-by
+ &middot;
+ last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
- .nav-controls
- = render 'main_links'
+ .nav-controls
+ = render 'main_links'
-- if @page.historical?
- .warning_message
- This is an old version of this page.
- You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
+ - if @page.historical?
+ .warning_message
+ This is an old version of this page.
+ You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-.wiki-holder.prepend-top-default.append-bottom-default
- .wiki
- = preserve do
- = render_wiki_content(@page)
+ .wiki-holder.prepend-top-default.append-bottom-default
+ .wiki
+ = preserve do
+ = render_wiki_content(@page)
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 30055002213..aa18e6f236f 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,7 +1,9 @@
%ul.nav-links.event-filter.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
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/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index eb2e1919e19..ea7162d4d63 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,7 +1,14 @@
+- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_namespace_project_refs_path(@project.namespace, @project), method: :get, class: "project-refs-form" do
- = select_tag "ref", grouped_options_refs, class: "project-refs-select select2 select2-sm"
= hidden_field_tag :destination, destination
- if defined?(path)
= hidden_field_tag :path, path
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
+ .dropdown
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
+ .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ = dropdown_title "Switch branch/tag"
+ = dropdown_filter "Search branches and tags"
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a25365a94f2..1ad95351005 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -9,7 +9,7 @@
= link_to edit_group_path(group), class: "btn" do
= icon('cogs')
- = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do
+ = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do
= icon('sign-out')
.stats
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/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 17e2a7e9290..c30bdb0ae91 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -35,13 +35,13 @@
.clearfix
.error-alert
-- if issuable.is_a?(Issue) && !issuable.project.private?
+- if issuable.is_a?(Issue)
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :confidential do
= f.check_box :confidential
- This issue is confidential and should only be visible to team members
+ This issue is confidential and should only be visible to team members with at least Reporter access.
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
- has_due_date = issuable.has_attribute?(:due_date)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index fb906de829a..adfab1af53e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,9 +1,21 @@
+- todo = issuable_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
- %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
+ - if current_user
+ %span.issuable-header-text.hide-collapsed.pull-left
+ Todo
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon
+ - if current_user
+ %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
+ %span.js-issuable-todo-text
+ - if todo
+ Mark Done
+ - else
+ Add Todo
+ = icon('spin spinner', class: 'hidden js-issuable-todo-loading')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
@@ -17,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'
@@ -50,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
@@ -73,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
@@ -112,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/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
new file mode 100644
index 00000000000..480e8ba6c85
--- /dev/null
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -0,0 +1,14 @@
+- member = source.members.find_by(user_id: current_user.id)
+- group_member = source.group.members.find_by(user_id: current_user.id) if source.respond_to?(:group) && source.group
+
+- unless group_member
+ - if member
+ - if member.request?
+ = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(member) },
+ class: 'btn access-request-button hidden-xs'
+ - else
+ = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+ method: :post,
+ class: 'btn access-request-button hidden-xs'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
new file mode 100644
index 00000000000..a884e78e6e7
--- /dev/null
+++ b/app/views/shared/members/_member.html.haml
@@ -0,0 +1,77 @@
+- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
+- show_controls = local_assigns.fetch(:show_controls, true)
+- user = member.user
+
+%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
+ %span{ class: ("list-item-name" if show_controls) }
+ - if user
+ = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
+ %strong
+ = link_to user.name, user_path(user)
+ %span.cgray= user.username
+
+ - if user == current_user
+ %span.label.label-success It's you
+
+ - if user.blocked?
+ %label.label.label-danger
+ %strong Blocked
+
+ - if member.request?
+ %span.cgray
+ – Requested
+ = time_ago_with_tooltip(member.requested_at)
+ - else
+ = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
+ %strong= member.invite_email
+ %span.cgray
+ – Invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_path(member.created_by)
+ = time_ago_with_tooltip(member.created_at)
+
+ - if show_controls && can?(current_user, action_member_permission(:admin, member), member.source)
+ = link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
+ method: :post,
+ class: 'btn-xs btn'
+
+ - if show_roles
+ %span.pull-right
+ %strong= member.human_access
+ - if show_controls
+ - if can?(current_user, action_member_permission(:update, member), member)
+ = button_tag icon('pencil'),
+ type: 'button',
+ class: 'btn-xs btn btn-grouped inline js-toggle-button',
+ title: 'Edit access level'
+
+ - if member.request?
+ &nbsp;
+ = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
+ method: :post,
+ class: 'btn-xs btn btn-success',
+ title: 'Grant access'
+
+ - if can?(current_user, action_member_permission(:destroy, member), member)
+ &nbsp;
+ - if current_user == user
+ = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(member.source) },
+ class: 'btn-xs btn btn-remove'
+ - else
+ = link_to icon('trash'), member,
+ remote: true,
+ method: :delete,
+ data: { confirm: remove_member_message(member) },
+ class: 'btn-xs btn btn-remove',
+ title: remove_member_title(member)
+
+ .edit-member.hide.js-toggle-content
+ %br
+ = form_for member, remote: true do |f|
+ .prepend-top-10
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
+ .prepend-top-10
+ = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
new file mode 100644
index 00000000000..e4bd2bdc265
--- /dev/null
+++ b/app/views/shared/members/_requests.html.haml
@@ -0,0 +1,8 @@
+- if members.any?
+ .panel.panel-default
+ .panel-heading
+ %strong= membership_source.name
+ access requests
+ %span.badge= members.size
+ %ul.content-list
+ = render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml
index c29d8ee6737..9c193f901e2 100644
--- a/app/views/shared/milestones/_merge_requests_tab.haml
+++ b/app/views/shared/milestones/_merge_requests_tab.haml
@@ -3,10 +3,10 @@
.row.prepend-top-default
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned')
+ = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing')
+ = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed')
+ = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true)
+ = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true, show_counter: true)
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 385c6596606..975c74f4ea6 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -10,6 +10,13 @@
open and
%strong= milestone.issues_visible_to_user(current_user).closed.size
closed
+ %strong= milestone.merge_requests.size
+ merge requests:
+ %span.milestone-stat
+ %strong= milestone.merge_requests.opened.size
+ open and
+ %strong= milestone.merge_requests.merged.size
+ merged
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
diff --git a/app/views/notifications/buttons/_notifications.html.haml b/app/views/shared/notifications/_button.html.haml
index 9aaa2890bdd..ff1cf966a9b 100644
--- a/app/views/notifications/buttons/_notifications.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,25 +1,25 @@
+- left_align = local_assigns[:left_align]
- if notification_setting
.dropdown.notification-dropdown.pull-right
- - url = notification_setting.persisted? ? notification_setting_path(notification_setting) : notification_settings_path(notification_setting)
-
- = form_for notification_setting, remote: true, html: { class: "inline", id: "notification-form" } do |f|
+ = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
.js-notification-toggle-btns
- .btn-group
- - if notification_setting.level != "custom"
- %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
- = icon("bell", class: "js-notification-loading")
- = notification_title(notification_setting.level)
- = icon("caret-down")
- - else
+ %div{ class: ("btn-group" if notification_setting.custom?) }
+ - if notification_setting.custom?
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
%span.caret
.sr-only Toggle dropdown
- = render "shared/notifications/notification_dropdown", notification_setting: notification_setting
+ - else
+ %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
+ = icon("bell", class: "js-notification-loading")
+ = notification_title(notification_setting.level)
+ = icon("caret-down")
+
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index f0b46c7a4c0..b704981e3db 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -12,17 +12,20 @@
= form_for notification_setting, html: { class: "custom-notifications-form" } do |f|
= hidden_setting_source_input(notification_setting)
.row
- .col-lg-3
+ .col-lg-4
%h4.prepend-top-0
Notification events
- .col-lg-9
- - NotificationSetting::EMAIL_EVENTS.each do |event, index|
- = index
+ %p
+ Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
+ = succeed "." do
+ %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails
+ .col-lg-8
+ - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
+ - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
.form-group
.checkbox{ class: ("prepend-top-0" if index == 0) }
- %label{ for: "events_#{event}" }
- = check_box("", event, { name: "notification_setting[#{event}]", class: "js-custom-notification-event", checked: notification_setting.events[event] })
-
+ %label{ for: field_id }
+ = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
%strong
= event.to_s.humanize
= icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
index e6e04b98c82..d3258ee64cb 100644
--- a/app/views/shared/notifications/_notification_dropdown.html.haml
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -1,12 +1,13 @@
-%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: notifications_menu_identifier("dropdown", notification_setting) }
+- left_align = local_assigns[:left_align]
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
- NotificationSetting.levels.each_key do |level|
- next if level == "custom"
- next if level == "global" && notification_setting.source.nil?
= notification_list_item(level, notification_setting)
- - unless notification_setting.custom?
- %li.divider
- %li
- %a.update-notification{ href: "#", role: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } }
- Custom
+ %li.divider
+ %li
+ %a.update-notification{ href: "#", role: "button", class: ("is-active" if notification_setting.custom?), data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } }
+ %strong.dropdown-menu-inner-title Custom
+ %span.dropdown-menu-inner-content= notification_description("custom")
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 2e08bb2ac08..3a9dd37dc7d 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -16,6 +16,12 @@
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description
+
+ - if @private_forks_count && @private_forks_count > 0
+ %li.project-row.private-forks-notice
+ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
+ %strong= pluralize(@private_forks_count, 'private fork')
+ %span you have no access to.
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else
.nothing-here-block No projects found
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 46af591fc43..cbb8dfb7829 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -4,11 +4,18 @@
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
%script#js-register-u2f-setup{ type: "text/template" }
- .row.append-bottom-10
- .col-md-3
- %a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device
- .col-md-9
- %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
+ - if current_user.two_factor_otp_enabled?
+ .row.append-bottom-10
+ .col-md-3
+ %button#js-setup-u2f-device.btn.btn-info Setup New U2F Device
+ .col-md-9
+ %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
+ - else
+ .row.append-bottom-10
+ .col-md-3
+ %button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup New U2F Device
+ .col-md-9
+ %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device.
%script#js-register-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
new file mode 100644
index 00000000000..c64ea108d52
--- /dev/null
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -0,0 +1,13 @@
+class ExpireBuildArtifactsWorker
+ include Sidekiq::Worker
+
+ def perform
+ Rails.logger.info 'Cleaning old build artifacts'
+
+ builds = Ci::Build.with_expired_artifacts
+ builds.find_each(batch_size: 50).each do |build|
+ Rails.logger.debug "Removing artifacts build #{build.id}..."
+ build.erase_artifacts!
+ end
+ end
+end
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/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index f2d12ba5a7d..98ddf5d0688 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -15,7 +15,7 @@ module RepositoryCheck
private
def check(project)
- if !git_fsck(project.repository)
+ if has_pushes?(project) && !git_fsck(project.repository)
false
elsif project.wiki_enabled?
# Historically some projects never had their wiki repos initialized;
@@ -44,5 +44,9 @@ module RepositoryCheck
false
end
end
+
+ def has_pushes?(project)
+ Project.with_push.exists?(project.id)
+ end
end
end
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
index ca594e77e7c..6828013b377 100644
--- a/app/workers/stuck_ci_builds_worker.rb
+++ b/app/workers/stuck_ci_builds_worker.rb
@@ -6,7 +6,7 @@ class StuckCiBuildsWorker
def perform
Rails.logger.info 'Cleaning stuck builds'
- builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
+ builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago)
builds.find_each(batch_size: 50).each do |build|
Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}"
build.drop
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/gitlab.yml.example b/config/gitlab.yml.example
index 1048ef6e243..75e1a3c1093 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -164,6 +164,9 @@ production: &base
# Flag stuck CI builds as failed
stuck_ci_builds_worker:
cron: "0 0 * * *"
+ # Remove expired build artifacts
+ expire_build_artifacts_worker:
+ cron: "50 * * * *"
# Periodically run 'git fsck' on all repositories. If started more than
# once per hour you will have concurrent 'git fsck' jobs.
repository_check_worker:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 436751b9d16..c6dc1e4ab38 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -215,7 +215,7 @@ Settings.gitlab.default_projects_features['container_registry'] = true if Settin
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
Settings.gitlab['restricted_signup_domains'] ||= []
-Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
@@ -279,6 +279,9 @@ Settings['cron_jobs'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_ci_builds_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['stuck_ci_builds_worker']['job_class'] = 'StuckCiBuildsWorker'
+Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
+Settings.cron_jobs['expire_build_artifacts_worker']['job_class'] = 'ExpireBuildArtifactsWorker'
Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *'
Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker'
@@ -288,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/chronic_duration.rb b/config/initializers/chronic_duration.rb
new file mode 100644
index 00000000000..b65b06c813a
--- /dev/null
+++ b/config/initializers/chronic_duration.rb
@@ -0,0 +1 @@
+ChronicDuration.raise_exceptions = true
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/initializers/metrics.rb b/config/initializers/metrics.rb
index f6509ee43f1..d159f4eded2 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -128,14 +128,25 @@ if Gitlab::Metrics.enabled?
config.instrument_instance_methods(API::Helpers)
config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
- # Iterate over each non-super private instance method to keep up to date if
- # internals change
- RepositoryCheck::SingleRepositoryWorker.private_instance_methods(false).each do |method|
- config.instrument_instance_method(RepositoryCheck::SingleRepositoryWorker, method)
- end
end
GC::Profiler.enable
Gitlab::Metrics::Sampler.new.start
+
+ module TrackNewRedisConnections
+ def connect(*args)
+ val = super
+
+ if current_transaction = Gitlab::Metrics::Transaction.current
+ current_transaction.increment(:new_redis_connections, 1)
+ end
+
+ val
+ end
+ end
+
+ class ::Redis::Client
+ prepend TrackNewRedisConnections
+ end
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index f1eec674888..7a2b9a7f6c1 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -23,6 +23,10 @@ Sidekiq.configure_server do |config|
config['pool'] = Sidekiq.options[:concurrency] + 2
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
+
+ # Avoid autoload issue such as 'Mail::Parsers::AddressStruct'
+ # https://github.com/mikel/mail/issues/912#issuecomment-214850355
+ Mail.eager_autoload!
end
Sidekiq.configure_client do |config|
diff --git a/config/routes.rb b/config/routes.rb
index 3dad841d498..e45293cdf7f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -30,6 +30,11 @@ Rails.application.routes.draw do
mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
end
+ concern :access_requestable do
+ post :request_access, on: :collection
+ post :approve_access_request, on: :member
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -174,6 +179,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
#
@@ -286,7 +295,7 @@ Rails.application.routes.draw do
post :repository_check
end
- resources :runner_projects
+ resources :runner_projects, only: [:create, :destroy]
end
end
@@ -351,6 +360,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
@@ -417,7 +433,7 @@ Rails.application.routes.draw do
end
scope module: :groups do
- resources :group_members, only: [:index, :create, :update, :destroy] do
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
end
@@ -457,8 +473,13 @@ 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
+ get :refs
end
scope module: :projects do
@@ -710,6 +731,8 @@ Rails.application.routes.draw do
end
end
+ resources :environments, only: [:index, :show, :new, :create, :destroy]
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
@@ -728,6 +751,7 @@ Rails.application.routes.draw do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
+ post :keep
end
end
@@ -771,7 +795,7 @@ Rails.application.routes.draw do
end
end
- resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do
+ resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
collection do
delete :leave
@@ -795,6 +819,8 @@ Rails.application.routes.draw do
end
end
+ resources :todos, only: [:create]
+
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb
new file mode 100644
index 00000000000..baac32f2d10
--- /dev/null
+++ b/db/fixtures/development/15_award_emoji.rb
@@ -0,0 +1,33 @@
+Gitlab::Seeder.quiet do
+ emoji = Gitlab::AwardEmoji.emojis.keys
+
+ Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue|
+ project = issue.project
+
+ project.team.users.sample(2).each do |user|
+ issue.create_award_emoji(emoji.sample, user)
+
+ issue.notes.sample(2).each do |note|
+ next if note.system?
+ note.create_award_emoji(emoji.sample, user)
+ end
+
+ print '.'
+ end
+ end
+
+ MergeRequest.order(Gitlab::Database.random).limit(MergeRequest.count / 2).each do |mr|
+ project = mr.project
+
+ project.team.users.sample(2).each do |user|
+ mr.create_award_emoji(emoji.sample, user)
+
+ mr.notes.sample(2).each do |note|
+ next if note.system?
+ note.create_award_emoji(emoji.sample, user)
+ end
+
+ print '.'
+ end
+ end
+end
diff --git a/db/migrate/20160314114439_add_requested_at_to_members.rb b/db/migrate/20160314114439_add_requested_at_to_members.rb
new file mode 100644
index 00000000000..273819d4cd8
--- /dev/null
+++ b/db/migrate/20160314114439_add_requested_at_to_members.rb
@@ -0,0 +1,5 @@
+class AddRequestedAtToMembers < ActiveRecord::Migration
+ def change
+ add_column :members, :requested_at, :datetime
+ end
+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/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb
index c226bc11f6c..95ee03611d9 100644
--- a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb
+++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb
@@ -1,10 +1,37 @@
# rubocop:disable all
class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration
- def change
- def up
- execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)"
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ migrate_postgresql
+ else
+ migrate_mysql
+ end
+ end
+
+ def down
+ add_column :notes, :is_award, :boolean
+ # This migration does NOT move the awards on notes, if the table is dropped in another migration, these notes will be lost.
+ execute "INSERT INTO notes (noteable_type, noteable_id, author_id, note, created_at, updated_at, is_award) (SELECT awardable_type, awardable_id, user_id, name, created_at, updated_at, TRUE FROM award_emoji)"
+ end
+
+ def migrate_postgresql
+ connection.transaction do
+ execute 'LOCK notes IN EXCLUSIVE MODE'
+ execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)"
execute "DELETE FROM notes WHERE is_award = true"
+ remove_column :notes, :is_award, :boolean
end
end
+
+ def migrate_mysql
+ execute 'LOCK TABLES notes WRITE, award_emoji WRITE;'
+ execute 'INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true);'
+ execute "DELETE FROM notes WHERE is_award = true"
+ remove_column :notes, :is_award, :boolean
+ ensure
+ execute 'UNLOCK TABLES'
+ end
end
diff --git a/db/migrate/20160416190505_remove_note_is_award.rb b/db/migrate/20160416190505_remove_note_is_award.rb
deleted file mode 100644
index dd24917feb9..00000000000
--- a/db/migrate/20160416190505_remove_note_is_award.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class RemoveNoteIsAward < ActiveRecord::Migration
- def change
- remove_column :notes, :is_award, :boolean
- end
-end
diff --git a/db/migrate/20160509091049_add_locked_to_ci_runner.rb b/db/migrate/20160509091049_add_locked_to_ci_runner.rb
new file mode 100644
index 00000000000..3fbaef3b7f0
--- /dev/null
+++ b/db/migrate/20160509091049_add_locked_to_ci_runner.rb
@@ -0,0 +1,13 @@
+class AddLockedToCiRunner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_runners, :locked, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:ci_runners, :locked)
+ end
+end
diff --git a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
new file mode 100644
index 00000000000..915167b038d
--- /dev/null
+++ b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb
@@ -0,0 +1,5 @@
+class AddArtifactsExpireDateToCiBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :artifacts_expire_at, :timestamp
+ end
+end
diff --git a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
new file mode 100644
index 00000000000..477b2106dea
--- /dev/null
+++ b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb
@@ -0,0 +1,6 @@
+class RemoveDeprecatedIssuesTrackerColumnsFromProjects < ActiveRecord::Migration
+ def change
+ remove_column :projects, :issues_tracker, :string, default: 'gitlab', null: false
+ remove_column :projects, :issues_tracker_id, :string
+ end
+end
diff --git a/db/migrate/20160610204157_add_deployments.rb b/db/migrate/20160610204157_add_deployments.rb
new file mode 100644
index 00000000000..cb144ea8a6d
--- /dev/null
+++ b/db/migrate/20160610204157_add_deployments.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ create_table :deployments, force: true do |t|
+ t.integer :iid, null: false
+ t.integer :project_id, null: false
+ t.integer :environment_id, null: false
+ t.string :ref, null: false
+ t.boolean :tag, null: false
+ t.string :sha, null: false
+ t.integer :user_id
+ t.integer :deployable_id
+ t.string :deployable_type
+ t.datetime :created_at
+ t.datetime :updated_at
+ end
+
+ add_index :deployments, :project_id
+ add_index :deployments, [:project_id, :iid], unique: true
+ add_index :deployments, [:project_id, :environment_id]
+ add_index :deployments, [:project_id, :environment_id, :iid]
+ end
+end
diff --git a/db/migrate/20160610204158_add_environments.rb b/db/migrate/20160610204158_add_environments.rb
new file mode 100644
index 00000000000..e1c71d173c4
--- /dev/null
+++ b/db/migrate/20160610204158_add_environments.rb
@@ -0,0 +1,17 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ create_table :environments, force: true do |t|
+ t.integer :project_id, null: false
+ t.string :name, null: false
+ t.datetime :created_at
+ t.datetime :updated_at
+ end
+
+ add_index :environments, [:project_id, :name]
+ end
+end
diff --git a/db/migrate/20160610211845_add_environment_to_builds.rb b/db/migrate/20160610211845_add_environment_to_builds.rb
new file mode 100644
index 00000000000..990e445ac55
--- /dev/null
+++ b/db/migrate/20160610211845_add_environment_to_builds.rb
@@ -0,0 +1,10 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironmentToBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :ci_builds, :environment, :string
+ end
+end
diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
new file mode 100644
index 00000000000..63f7392e54f
--- /dev/null
+++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
@@ -0,0 +1,9 @@
+class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :members, :requested_at
+ end
+end
diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
new file mode 100644
index 00000000000..bd0463886bc
--- /dev/null
+++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
@@ -0,0 +1,9 @@
+class SetMissingStageOnCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ update_column_in_batches(:ci_builds, :stage, :test) do |table, query|
+ query.where(table[:stage].eq(nil))
+ 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/migrate/20160616102642_remove_duplicated_keys.rb b/db/migrate/20160616102642_remove_duplicated_keys.rb
new file mode 100644
index 00000000000..00a45d7fe73
--- /dev/null
+++ b/db/migrate/20160616102642_remove_duplicated_keys.rb
@@ -0,0 +1,19 @@
+# rubocop:disable all
+class RemoveDuplicatedKeys < ActiveRecord::Migration
+ def up
+ select_all("SELECT fingerprint FROM #{quote_table_name(:keys)} GROUP BY fingerprint HAVING COUNT(*) > 1").each do |row|
+ fingerprint = connection.quote(row['fingerprint'])
+ execute(%Q{
+ DELETE FROM keys
+ WHERE fingerprint = #{fingerprint}
+ AND id != (
+ SELECT id FROM (
+ SELECT max(id) AS id
+ FROM keys
+ WHERE fingerprint = #{fingerprint}
+ ) max_ids
+ )
+ })
+ end
+ end
+end
diff --git a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
new file mode 100644
index 00000000000..4bb4204cebd
--- /dev/null
+++ b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
@@ -0,0 +1,21 @@
+class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/250
+ # That MR was added on gitlab-ee so we need to check if the index
+ # already exists because we want to do is create an unique index instead.
+
+ def up
+ if index_exists?(:keys, :fingerprint)
+ remove_index :keys, :fingerprint
+ end
+ end
+
+ def down
+ unless index_exists?(:keys, :fingerprint)
+ add_concurrent_index :keys, :fingerprint
+ end
+ end
+end
diff --git a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
new file mode 100644
index 00000000000..e35af38aac3
--- /dev/null
+++ b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
@@ -0,0 +1,13 @@
+class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :keys, :fingerprint, unique: true
+ end
+
+ def down
+ remove_index :keys, :fingerprint
+ end
+end
diff --git a/db/migrate/20160614301627_add_events_to_notification_settings.rb b/db/migrate/20160617301627_add_events_to_notification_settings.rb
index 609596f45e4..609596f45e4 100644
--- a/db/migrate/20160614301627_add_events_to_notification_settings.rb
+++ b/db/migrate/20160617301627_add_events_to_notification_settings.rb
diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb
new file mode 100644
index 00000000000..dfa5110dea4
--- /dev/null
+++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexOnRunnersLocked < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :ci_runners, :locked
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index aac327797e7..7a8377f687c 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: 20160608155312) do
+ActiveRecord::Schema.define(version: 20160620115026) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -144,9 +144,9 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.text "commands"
t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
+ t.boolean "allow_failure", default: false, null: false
t.string "stage"
t.integer "trigger_request_id"
t.integer "stage_idx"
@@ -161,6 +161,8 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.text "artifacts_metadata"
t.integer "erased_by_id"
t.datetime "erased_at"
+ t.string "environment"
+ t.datetime "artifacts_expire_at"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -285,9 +287,11 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.string "platform"
t.string "architecture"
t.boolean "run_untagged", default: true, null: false
+ t.boolean "locked", default: false, null: false
end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
@@ -381,6 +385,25 @@ ActiveRecord::Schema.define(version: 20160608155312) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
+ create_table "deployments", force: :cascade do |t|
+ t.integer "iid", null: false
+ t.integer "project_id", null: false
+ t.integer "environment_id", null: false
+ t.string "ref", null: false
+ t.boolean "tag", null: false
+ t.string "sha", null: false
+ t.integer "user_id"
+ t.integer "deployable_id"
+ t.string "deployable_type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
+ add_index "deployments", ["project_id", "environment_id"], name: "index_deployments_on_project_id_and_environment_id", using: :btree
+ add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
+ add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree
+
create_table "emails", force: :cascade do |t|
t.integer "user_id", null: false
t.string "email", null: false
@@ -391,6 +414,15 @@ ActiveRecord::Schema.define(version: 20160608155312) do
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
+ create_table "environments", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "name", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
+
create_table "events", force: :cascade do |t|
t.string "target_type"
t.integer "target_id"
@@ -477,6 +509,7 @@ ActiveRecord::Schema.define(version: 20160608155312) do
end
add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
+ add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
create_table "label_links", force: :cascade do |t|
@@ -536,11 +569,13 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.string "invite_email"
t.string "invite_token"
t.datetime "invite_accepted_at"
+ t.datetime "requested_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree
add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
+ add_index "members", ["requested_at"], name: "index_members_on_requested_at", using: :btree
add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
add_index "members", ["type"], name: "index_members_on_type", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
@@ -670,11 +705,12 @@ ActiveRecord::Schema.define(version: 20160608155312) do
create_table "notification_settings", force: :cascade do |t|
t.integer "user_id", null: false
- t.integer "source_id", null: false
- t.string "source_type", null: false
+ t.integer "source_id"
+ t.string "source_type"
t.integer "level", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.text "events"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
@@ -724,6 +760,19 @@ ActiveRecord::Schema.define(version: 20160608155312) 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
@@ -747,39 +796,37 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
- t.string "issues_tracker", default: "gitlab", null: false
- t.string "issues_tracker_id"
- t.boolean "snippets_enabled", default: true, null: false
+ t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
+ t.integer "star_count", default: 0, null: false
t.string "import_type"
t.string "import_source"
t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
- t.boolean "builds_enabled", default: true, null: false
- t.boolean "shared_runners_enabled", default: true, null: false
+ t.boolean "builds_enabled", default: true, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
t.string "build_coverage_regex"
- t.boolean "build_allow_git_fetch", default: true, null: false
- t.integer "build_timeout", default: 3600, null: false
+ t.boolean "build_allow_git_fetch", default: true, null: false
+ t.integer "build_timeout", default: 3600, null: false
t.boolean "pending_delete", default: false
- t.boolean "public_builds", default: true, null: false
+ t.boolean "public_builds", default: true, null: false
t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
t.boolean "container_registry_enabled"
- t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
+ t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
t.boolean "has_external_issue_tracker"
end
@@ -988,7 +1035,6 @@ ActiveRecord::Schema.define(version: 20160608155312) do
t.boolean "can_create_team", default: true, null: false
t.string "state"
t.integer "color_scheme_id", default: 1, null: false
- t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at"
t.integer "created_by_id"
t.datetime "last_credential_check_at"
@@ -1066,5 +1112,6 @@ ActiveRecord::Schema.define(version: 20160608155312) 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/README.md b/doc/README.md
index 5d89d0c9821..f1283cea0ad 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,17 +3,18 @@
## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API.
-- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples.
+- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
+- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
+- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
-- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab
+- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
- [Permissions](permissions/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
-- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
@@ -24,15 +25,15 @@
external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
- [Install](install/README.md) Requirements, directory structures and installation from source.
-- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components
+- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
-- [Operations](operations/README.md) Keeping GitLab up and running
+- [Operations](operations/README.md) Keeping GitLab up and running.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
-- [Repository checks](administration/repository_checks.md) Periodic Git repository checks
+- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
@@ -41,11 +42,11 @@
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics
-- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint
-- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs
-- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability
-- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab
+- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint.
+- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
+- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
+- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
## Contributor documentation
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 7870669fa77..d5d43303454 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -22,6 +22,7 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr
- [Disable Container Registry per project](#disable-container-registry-per-project)
- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide)
- [Container Registry storage path](#container-registry-storage-path)
+- [Container Registry storage driver](#container-registry-storage-driver)
- [Storage limitations](#storage-limitations)
- [Changelog](#changelog)
@@ -84,6 +85,17 @@ GitLab does not ship with a Registry init file. Hence, [restarting GitLab][resta
will not restart the Registry should you modify its settings. Read the upstream
documentation on how to achieve that.
+The Docker Registry configuration will need `container_registry` as the service and `https://gitlab.example.com/jwt/auth` as the realm:
+
+```
+auth:
+ token:
+ realm: https://gitlab.example.com/jwt/auth
+ service: container_registry
+ issuer: gitlab-issuer
+ rootcertbundle: /root/certs/certbundle
+```
+
## Container Registry domain configuration
There are two ways you can configure the Registry's external domain.
@@ -306,8 +318,12 @@ the Container Registry by themselves, follow the steps below.
## Container Registry storage path
-To change the storage path where Docker images will be stored, follow the
-steps below.
+>**Note:**
+For configuring storage in the cloud instead of the filesystem, see the
+[storage driver configuration](#container-registry-storage-driver).
+
+If you want to store your images on the filesystem, you can change the storage
+path for the Container Registry, follow the steps below.
This path is accessible to:
@@ -349,6 +365,72 @@ The default location where images are stored in source installations, is
1. Save the file and [restart GitLab][] for the changes to take effect.
+## Container Registry storage driver
+
+You can configure the Container Registry to use a different storage backend by
+configuring a different storage driver. By default the GitLab Container Registry
+is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path)
+configuration.
+
+The different supported drivers are:
+
+| Driver | Description |
+|------------|-------------------------------------|
+| filesystem | Uses a path on the local filesystem |
+| azure | Microsoft Azure Blob Storage |
+| gcs | Google Cloud Storage |
+| s3 | Amazon Simple Storage Service |
+| swift | OpenStack Swift Object Storage |
+| oss | Aliyun OSS |
+
+Read more about the individual driver's config options in the
+[Docker Registry docs][storage-config].
+
+> **Warning** GitLab will not backup Docker images that are not stored on the
+filesystem. Remember to enable backups with your object storage provider if
+desired.
+
+---
+
+**Omnibus GitLab installations**
+
+To configure the storage driver in Omnibus:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['storage'] = {
+ 's3' => {
+ 'accesskey' => 's3-access-key',
+ 'secretkey' => 's3-secret-key-for-access-key',
+ 'bucket' => 'your-s3-bucket'
+ }
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+Configuring the storage driver is done in your registry config YML file created
+when you [deployed your docker registry][registry-deploy].
+
+Example:
+
+```
+storage:
+ s3:
+ accesskey: 'AKIAKIAKI'
+ secretkey: 'secret123'
+ bucket: 'gitlab-registry-bucket-AKIAKIAKI'
+ cache:
+ blobdescriptor: inmemory
+ delete:
+ enabled: true
+```
+
## Storage limitations
Currently, there is no storage limitation, which means a user can upload an
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
new file mode 100644
index 00000000000..c212059b9d5
--- /dev/null
+++ b/doc/administration/raketasks/project_import_export.md
@@ -0,0 +1,33 @@
+# Project import/export
+
+>**Note:**
+ - This feature was [introduced][ce-3050] in GitLab 8.9
+ - Importing will not be possible if the import instance version is lower
+ than that of the exporter.
+ - For existing installations, the project import option has to be enabled in
+ application settings (`/admin/application_settings`) under 'Import sources'.
+ - The exports are stored in a temporary [shared directory][tmp] and are deleted
+ every 24 hours by a specific worker.
+
+The GitLab Import/Export version can be checked by using:
+
+```bash
+# Omnibus installations
+sudo gitlab-rake gitlab:import_export:version
+
+# Installations from source
+bundle exec rake gitlab:import_export:version RAILS_ENV=production
+```
+
+The current list of DB tables that will get exported can be listed by using:
+
+```bash
+# Omnibus installations
+sudo gitlab-rake gitlab:import_export:data
+
+# Installations from source
+bundle exec rake gitlab:import_export:data RAILS_ENV=production
+```
+
+[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[tmp]: ../../development/shared_files.md
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index a776cd3f05e..b71f8fabbc8 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -147,7 +147,8 @@ bt
To output a backtrace from all threads at once:
```
-apply all thread bt
+set pagination off
+thread apply all bt
```
Once you're done debugging with `gdb`, be sure to detach from the process and
diff --git a/doc/api/README.md b/doc/api/README.md
index 27c5962decf..288f7f9ee69 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -8,42 +8,49 @@ under [`/lib/api`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api).
Documentation for various API resources can be found separately in the
following locations:
-- [Users](users.md)
-- [Session](session.md)
-- [Projects](projects.md) including setting Webhooks
-- [Project Snippets](project_snippets.md)
-- [Services](services.md)
-- [Repositories](repositories.md)
-- [Repository Files](repository_files.md)
-- [Commits](commits.md)
-- [Tags](tags.md)
+- [Award Emoji](award_emoji.md)
- [Branches](branches.md)
-- [Merge Requests](merge_requests.md)
+- [Builds](builds.md)
+- [Build triggers](build_triggers.md)
+- [Build Variables](build_variables.md)
+- [Commits](commits.md)
+- [Deploy Keys](deploy_keys.md)
+- [Groups](groups.md)
- [Issues](issues.md)
+- [Keys](keys.md)
- [Labels](labels.md)
+- [Merge Requests](merge_requests.md)
- [Milestones](milestones.md)
-- [Notes](notes.md) (comments)
-- [Deploy Keys](deploy_keys.md)
-- [System Hooks](system_hooks.md)
-- [Groups](groups.md)
+- [Open source license templates](licenses.md)
- [Namespaces](namespaces.md)
-- [Settings](settings.md)
-- [Keys](keys.md)
-- [Builds](builds.md)
-- [Build triggers](build_triggers.md)
-- [Build Variables](build_variables.md)
+- [Notes](notes.md) (comments)
+- [Projects](projects.md) including setting Webhooks
+- [Project Snippets](project_snippets.md)
+- [Repositories](repositories.md)
+- [Repository Files](repository_files.md)
- [Runners](runners.md)
-- [Open source license templates](licenses.md)
+- [Services](services.md)
+- [Session](session.md)
+- [Settings](settings.md)
+- [Sidekiq metrics](sidekiq_metrics.md)
+- [System Hooks](system_hooks.md)
+- [Tags](tags.md)
+- [Users](users.md)
+
+### Internal CI API
+
+The following documentation is for the [internal CI API](ci/README.md):
+
+- [Builds](ci/builds.md)
+- [Runners](ci/runners.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
{
@@ -51,42 +58,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
@@ -323,3 +344,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/award_emoji.md b/doc/api/award_emoji.md
new file mode 100644
index 00000000000..b44f8cfd628
--- /dev/null
+++ b/doc/api/award_emoji.md
@@ -0,0 +1,367 @@
+# Award Emoji
+
+ >**Note:** This feature was introduced in GitLab 8.9
+
+An awarded emoji tells a thousand words, and can be awarded on issues, merge
+requests and notes/comments. Issues, merge requests and notes are further called
+`awardables`.
+
+## Issues and merge requests
+
+### List an awardable's award emoji
+
+Gets a list of all award emoji
+
+```
+GET /projects/:id/issues/:issue_id/award_emoji
+GET /projects/:id/merge_requests/:merge_request_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID of an awardable |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 4,
+ "name": "1234",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-15T10:09:34.206Z",
+ "updated_at": "2016-06-15T10:09:34.206Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+ },
+ {
+ "id": 1,
+ "name": "microphone",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.177Z",
+ "updated_at": "2016-06-15T10:09:34.177Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+ }
+]
+```
+
+### Get single issue note
+
+Gets a single award emoji
+
+```
+GET /projects/:id/issues/:issue_id/award_emoji/:award_id
+GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID of an awardable |
+| `award_id` | integer | yes | The ID of the award emoji |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
+```
+
+Example Response:
+
+```json
+{
+ "id": 1,
+ "name": "microphone",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.177Z",
+ "updated_at": "2016-06-15T10:09:34.177Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+}
+```
+
+### Award a new emoji
+
+This end point creates an award emoji on the specified resource
+
+```
+POST /projects/:id/issues/:issue_id/award_emoji
+POST /projects/:id/merge_requests/:merge_request_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `awardable_id` | integer | yes | The ID of an awardable |
+| `name` | string | yes | The name of the emoji, without colons |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
+```
+
+Example Response:
+
+```json
+{
+ "id": 344,
+ "name": "blowfish",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T17:47:29.266Z",
+ "updated_at": "2016-06-17T17:47:29.266Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+}
+```
+
+### Delete an award emoji
+
+Sometimes its just not meant to be, and you'll have to remove your award. Only available to
+admins or the author of the award. Status code 200 on success, 401 if unauthorized.
+
+```
+DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
+DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `award_id` | integer | yes | The ID of a award_emoji |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
+```
+
+Example Response:
+
+```json
+{
+ "id": 344,
+ "name": "blowfish",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T17:47:29.266Z",
+ "updated_at": "2016-06-17T17:47:29.266Z",
+ "awardable_id": 80,
+ "awardable_type": "Issue"
+}
+```
+
+## Award Emoji on Notes
+
+The endpoints documented above are available for Notes as well. Notes
+are a sub-resource of Issues and Merge Requests. The examples below
+describe working with Award Emoji on notes for an Issue, but can be
+easily adapted for notes on a Merge Request.
+
+### List a note's award emoji
+
+```
+GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of an note |
+
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 2,
+ "name": "mood_bubble_lightning",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.197Z",
+ "updated_at": "2016-06-15T10:09:34.197Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+ }
+]
+```
+
+### Get single note's award emoji
+
+```
+GET /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `award_id` | integer | yes | The ID of the award emoji |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
+```
+
+Example Response:
+
+```json
+{
+ "id": 2,
+ "name": "mood_bubble_lightning",
+ "user": {
+ "name": "User 4",
+ "username": "user4",
+ "id": 26,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/7e65550957227bd38fe2d7fbc6fd2f7b?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/user4"
+ },
+ "created_at": "2016-06-15T10:09:34.197Z",
+ "updated_at": "2016-06-15T10:09:34.197Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+}
+```
+
+### Award a new emoji on a note
+
+```
+POST /projects/:id/issues/:issue_id/notes/:note_id/award_emoji
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `name` | string | yes | The name of the emoji, without colons |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
+```
+
+Example Response:
+
+```json
+{
+ "id": 345,
+ "name": "rocket",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T19:59:55.888Z",
+ "updated_at": "2016-06-17T19:59:55.888Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+}
+```
+
+### Delete an award emoji
+
+Sometimes its just not meant to be, and you'll have to remove your award. Only available to
+admins or the author of the award. Status code 200 on success, 401 if unauthorized.
+
+```
+DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of an issue |
+| `note_id` | integer | yes | The ID of a note |
+| `award_id` | integer | yes | The ID of a award_emoji |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
+```
+
+Example Response:
+
+```json
+{
+ "id": 345,
+ "name": "rocket",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/u/root"
+ },
+ "created_at": "2016-06-17T19:59:55.888Z",
+ "updated_at": "2016-06-17T19:59:55.888Z",
+ "awardable_id": 1,
+ "awardable_type": "Note"
+}
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 5669bd0cdda..de998944352 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -21,85 +21,85 @@ Example of response
```json
[
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.802Z",
- "artifacts_file": {
- "filename": "artifacts.zip",
- "size": 1000
- },
- "finished_at": "2015-12-24T17:54:27.895Z",
- "id": 7,
- "name": "teaspoon",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:27.722Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/u/root",
- "website_url": ""
- }
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
},
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.727Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:24.921Z",
- "id": 6,
- "name": "spinach:other",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:24.729Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/u/root",
- "website_url": ""
- }
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "id": 6,
+ "name": "spinach:other",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
}
+ }
]
```
@@ -125,68 +125,68 @@ Example of response
```json
[
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
- "finished_at": "2016-01-11T10:14:09.526Z",
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": null,
- "status": "canceled",
- "tag": false,
- "user": null
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
},
- {
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.957Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:33.913Z",
- "id": 9,
- "name": "brakeman",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:33.727Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/u/root",
- "website_url": ""
- }
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.957Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:33.913Z",
+ "id": 9,
+ "name": "brakeman",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:33.727Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
}
+ }
]
```
@@ -211,42 +211,42 @@ Example of response
```json
{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2015-12-24T15:51:21.880Z",
- "artifacts_file": null,
- "finished_at": "2015-12-24T17:54:31.198Z",
- "id": 8,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": "2015-12-24T17:54:30.733Z",
- "status": "failed",
- "tag": false,
- "user": {
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "bio": null,
- "created_at": "2015-12-21T13:14:24.077Z",
- "id": 1,
- "is_admin": true,
- "linkedin": "",
- "name": "Administrator",
- "skype": "",
- "state": "active",
- "twitter": "",
- "username": "root",
- "web_url": "http://gitlab.dev/u/root",
- "website_url": ""
- }
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.880Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:31.198Z",
+ "id": 8,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:30.733Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
}
```
@@ -323,28 +323,28 @@ Example of response
```json
{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
- "finished_at": "2016-01-11T10:14:09.526Z",
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": null,
- "status": "canceled",
- "tag": false,
- "user": null
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
}
```
@@ -369,28 +369,28 @@ Example of response
```json
{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
- "finished_at": null,
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "started_at": null,
- "status": "pending",
- "tag": false,
- "user": null
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "pending",
+ "tag": false,
+ "user": null
}
```
@@ -419,27 +419,77 @@ Example of response
```json
{
- "commit": {
- "author_email": "admin@example.com",
- "author_name": "Administrator",
- "created_at": "2015-12-24T16:51:14.000+01:00",
- "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
- "message": "Test the CI integration.",
- "short_id": "0ff3ae19",
- "title": "Test the CI integration."
- },
- "coverage": null,
- "download_url": null,
- "id": 69,
- "name": "rubocop",
- "ref": "master",
- "runner": null,
- "stage": "test",
- "created_at": "2016-01-11T10:13:33.506Z",
- "started_at": "2016-01-11T10:13:33.506Z",
- "finished_at": "2016-01-11T10:15:10.506Z",
- "status": "failed",
- "tag": false,
- "user": null
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "download_url": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "started_at": "2016-01-11T10:13:33.506Z",
+ "finished_at": "2016-01-11T10:15:10.506Z",
+ "status": "failed",
+ "tag": false,
+ "user": null
+}
+```
+
+## Keep artifacts
+
+Prevents artifacts from being deleted when expiration is set.
+
+```
+POST /projects/:id/builds/:build_id/artifacts/keep
+```
+
+Parameters
+
+| Attribute | Type | required | Description |
+|-------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+Example request:
+
+```
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
+```
+
+Example response:
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "download_url": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "started_at": "2016-01-11T10:13:33.506Z",
+ "finished_at": "2016-01-11T10:15:10.506Z",
+ "status": "failed",
+ "tag": false,
+ "user": null
}
```
diff --git a/doc/api/ci/README.md b/doc/api/ci/README.md
new file mode 100644
index 00000000000..96a281e27c8
--- /dev/null
+++ b/doc/api/ci/README.md
@@ -0,0 +1,24 @@
+# GitLab CI API
+
+## Purpose
+
+The main purpose of GitLab CI API is to provide the necessary data and context
+for GitLab CI Runners.
+
+All relevant information about the consumer API can be found in a
+[separate document](../../api/README.md).
+
+## API Prefix
+
+The current CI API prefix is `/ci/api/v1`.
+
+You need to prepend this prefix to all examples in this documentation, like:
+
+```bash
+GET /ci/api/v1/builds/:id/artifacts
+```
+
+## Resources
+
+- [Builds](builds.md)
+- [Runners](runners.md)
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
new file mode 100644
index 00000000000..d779463fd8c
--- /dev/null
+++ b/doc/api/ci/builds.md
@@ -0,0 +1,138 @@
+# Builds API
+
+API used by runners to receive and update builds.
+
+>**Note:**
+This API is intended to be used only by Runners as their own
+communication channel. For the consumer API see the
+[Builds API](../builds.md).
+
+## Authentication
+
+This API uses two types of authentication:
+
+1. Unique Runner's token which is the token assigned to the Runner after it
+ has been registered.
+
+2. Using the build authorization token.
+ This is project's CI token that can be found under the **Builds** section of
+ a project's settings. The build authorization token can be passed as a
+ parameter or a value of `BUILD-TOKEN` header.
+
+These two methods of authentication are interchangeable.
+
+## Builds
+
+### Runs oldest pending build by runner
+
+```
+POST /ci/api/v1/builds/register
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `token` | string | yes | Unique runner token |
+
+
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
+```
+
+### Update details of an existing build
+
+```
+PUT /ci/api/v1/builds/:id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | Unique runner token |
+| `state` | string | no | The state of a build |
+| `trace` | string | no | The trace of a build |
+
+```
+curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
+```
+
+### Incremental build trace update
+
+Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
+with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
+must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
+Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
+
+For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
+header and a trace part covered by this range.
+
+For a valid update API will return `202` response with:
+* `Build-Status: {status}` header containing current status of the build,
+* `Range: 0-{length}` header with the current trace length.
+
+```
+PATCH /ci/api/v1/builds/:id/trace.txt
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a build |
+
+Headers:
+
+| Attribute | Type | Required | Description |
+|-----------------|---------|----------|-----------------------------------|
+| `BUILD-TOKEN` | string | yes | The build authorization token |
+| `Content-Range` | string | yes | Bytes range of trace that is sent |
+
+```
+curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
+```
+
+
+### Upload artifacts to build
+
+```
+POST /ci/api/v1/builds/:id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+| `file` | mixed | yes | Artifacts file |
+
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
+```
+
+### Download the artifacts file from build
+
+```
+GET /ci/api/v1/builds/:id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+
+```
+curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
+
+### Remove the artifacts file from build
+
+```
+DELETE /ci/api/v1/builds/:id/artifacts
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| ` id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+
+```
+curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
new file mode 100644
index 00000000000..96b3c42f773
--- /dev/null
+++ b/doc/api/ci/runners.md
@@ -0,0 +1,57 @@
+# Runners API
+
+API used by Runners to register and delete themselves.
+
+>**Note:**
+This API is intended to be used only by Runners as their own
+communication channel. For the consumer API see the
+[new Runners API](../runners.md).
+
+## Authentication
+
+This API uses two types of authentication:
+
+1. Unique Runner's token, which is the token assigned to the Runner after it
+ has been registered.
+
+2. Using Runners' registration token.
+ This is a token that can be found in project's settings.
+ It can also be found in the **Admin > Runners** settings area.
+ There are two types of tokens you can pass: shared Runner registration
+ token or project specific registration token.
+
+## Register a new runner
+
+Used to make GitLab CI aware of available runners.
+
+```sh
+POST /ci/api/v1/runners/register
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | --------- | ----------- |
+| `token` | string | yes | Runner's registration token |
+
+Example request:
+
+```sh
+curl -X POST "https://gitlab.example.com/ci/api/v1/runners/register" -F "token=t0k3n"
+```
+
+## Delete a Runner
+
+Used to remove a Runner.
+
+```sh
+DELETE /ci/api/v1/runners/delete
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | --------- | ----------- |
+| `token` | string | yes | Runner's registration token |
+
+Example request:
+
+```sh
+curl -X DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" -F "token=t0k3n"
+```
diff --git a/doc/api/issues.md b/doc/api/issues.md
index fc7a7ae0c0c..0bc82ef9edb 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -365,6 +365,9 @@ target project is not found, error `404` is returned. If the target project
equals the source project or the user has insufficient permissions to move an
issue, error `400` together with an explaining error message is returned.
+If a given label and/or milestone with the same name also exists in the target
+project, it will then be assigned to the issue that is being moved.
+
```
POST /projects/:id/issues/:issue_id/move
```
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/ci/README.md b/doc/ci/README.md
index 4abc45bf9bb..3dd4e2bc230 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -5,6 +5,8 @@
- [Get started with GitLab CI](quick_start/README.md)
- [CI examples for various languages](examples/README.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
+- [Pipelines and builds](pipelines.md)
+- [Environments and deployments](environments.md)
- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
- [Configure a Runner, the application that runs your builds](runners/README.md)
- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
@@ -14,5 +16,5 @@
- [Trigger builds through the API](triggers/README.md)
- [Build artifacts](build_artifacts/README.md)
- [User permissions](permissions/README.md)
-- [API](api/README.md)
+- [API](../../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
index aea808007fc..4ca8d92d7cc 100644
--- a/doc/ci/api/README.md
+++ b/doc/ci/api/README.md
@@ -1,22 +1,3 @@
# GitLab CI API
-## Purpose
-
-Main purpose of GitLab CI API is to provide necessary data and context for
-GitLab CI Runners.
-
-For consumer API take a look at this [documentation](../../api/README.md) where
-you will find all relevant information.
-
-## API Prefix
-
-Current CI API prefix is `/ci/api/v1`.
-
-You need to prepend this prefix to all examples in this documentation, like:
-
- GET /ci/api/v1/builds/:id/artifacts
-
-## Resources
-
-- [Builds](builds.md)
-- [Runners](runners.md)
+This document was moved to a [new location](../../api/ci/README.md).
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
index 79761a893da..f5bd3181c02 100644
--- a/doc/ci/api/builds.md
+++ b/doc/ci/api/builds.md
@@ -1,139 +1,3 @@
# Builds API
-API used by runners to receive and update builds.
-
-_**Note:** This API is intended to be used only by Runners as their own
-communication channel. For the consumer API see the
-[Builds API](../../api/builds.md)._
-
-## Authentication
-
-This API uses two types of authentication:
-
-1. Unique runner's token
-
- Token assigned to runner after it has been registered.
-
-2. Using build authorization token
-
- This is project's CI token that can be found in Continuous Integration
- project settings.
-
- Build authorization token can be passed as a parameter or a value of
- `BUILD-TOKEN` header. This method are interchangeable.
-
-## Builds
-
-### Runs oldest pending build by runner
-
-```
-POST /ci/api/v1/builds/register
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|---------------------|
-| `token` | string | yes | Unique runner token |
-
-
-```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
-```
-
-### Update details of an existing build
-
-```
-PUT /ci/api/v1/builds/:id
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|----------------------|
-| `id` | integer | yes | The ID of a project |
-| `token` | string | yes | Unique runner token |
-| `state` | string | no | The state of a build |
-| `trace` | string | no | The trace of a build |
-
-```
-curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
-```
-
-### Incremental build trace update
-
-Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
-with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
-must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
-Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
-
-For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
-header and a trace part covered by this range.
-
-For a valid update API will return `202` response with:
-* `Build-Status: {status}` header containing current status of the build,
-* `Range: 0-{length}` header with the current trace length.
-
-```
-PATCH /ci/api/v1/builds/:id/trace.txt
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|----------------------|
-| `id` | integer | yes | The ID of a build |
-
-Headers:
-
-| Attribute | Type | Required | Description |
-|-----------------|---------|----------|-----------------------------------|
-| `BUILD-TOKEN` | string | yes | The build authorization token |
-| `Content-Range` | string | yes | Bytes range of trace that is sent |
-
-```
-curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
-```
-
-
-### Upload artifacts to build
-
-```
-POST /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| `id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-| `file` | mixed | yes | Artifacts file |
-
-```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
-```
-
-### Download the artifacts file from build
-
-```
-GET /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| `id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-
-```
-curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
-```
-
-### Remove the artifacts file from build
-
-```
-DELETE /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| ` id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-
-```
-curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
-```
+This document was moved to a [new location](../../api/ci/builds.md).
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
index 2f01da4bd76..b14ea99db76 100644
--- a/doc/ci/api/runners.md
+++ b/doc/ci/api/runners.md
@@ -1,46 +1,3 @@
# Runners API
-API used by runners to register and delete themselves.
-
-_**Note:** This API is intended to be used only by Runners as their own
-communication channel. For the consumer API see the
-[new Runners API](../../api/runners.md)._
-
-## Authentication
-
-This API uses two types of authentication:
-
-1. Unique runner's token
-
- Token assigned to runner after it has been registered.
-
-2. Using runners' registration token
-
- This is a token that can be found in project's settings.
- It can be also found in Admin area &raquo; Runners settings.
-
- There are two types of tokens you can pass - shared runner registration
- token or project specific registration token.
-
-## Runners
-
-### Register a new runner
-
-Used to make GitLab CI aware of available runners.
-
- POST /ci/api/v1/runners/register
-
-Parameters:
-
- * `token` (required) - Registration token
-
-
-### Delete a runner
-
-Used to remove runner.
-
- DELETE /ci/api/v1/runners/delete
-
-Parameters:
-
- * `token` (required) - Unique runner token
+This document was moved to a [new location](../../api/ci/runners.md).
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index ca52a483a59..7f83f846454 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -4,14 +4,14 @@ GitLab CI allows you to use Docker Engine to build and test docker-based project
**This also allows to you to use `docker-compose` and other docker-enabled tools.**
-This is one of new trends in Continuous Integration/Deployment to:
+One of the new trends in Continuous Integration/Deployment is to:
-1. create application image,
-1. run test against created image,
-1. push image to remote registry,
-1. deploy server from pushed image
+1. create an application image,
+1. run tests against the created image,
+1. push image to a remote registry, and
+1. deploy to a server from the pushed image.
-It's also useful in case when your application already has the `Dockerfile` that can be used to create and test image:
+It's also useful when your application already has the `Dockerfile` that can be used to create and test an image:
```bash
$ docker build -t my-image dockerfiles/
$ docker run my-docker-image /script/to/run/tests
@@ -19,24 +19,25 @@ $ docker tag my-image my-registry:5000/my-image
$ docker push my-registry:5000/my-image
```
-However, this requires special configuration of GitLab Runner to enable `docker` support during build.
-**This requires running GitLab Runner in privileged mode which can be harmful when untrusted code is run.**
+This requires special configuration of GitLab Runner to enable `docker` support during builds.
-There are two methods to enable the use of `docker build` and `docker run` during build.
+## Runner Configuration
-## 1. Use shell executor
+There are three methods to enable the use of `docker build` and `docker run` during builds; each with their own tradeoffs.
+
+### Use shell executor
The simplest approach is to install GitLab Runner in `shell` execution mode.
-GitLab Runner then executes build scripts as `gitlab-runner` user.
+GitLab Runner then executes build scripts as the `gitlab-runner` user.
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
1. During GitLab Runner installation select `shell` as method of executing build scripts or use command:
```bash
- $ sudo gitlab-runner register -n \
+ $ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
- --token RUNNER_TOKEN \
+ --registration-token REGISTRATION_TOKEN \
--executor shell
--description "My Runner"
```
@@ -70,16 +71,18 @@ GitLab Runner then executes build scripts as `gitlab-runner` user.
5. You can now use `docker` command and install `docker-compose` if needed.
-6. However, by adding `gitlab-runner` to `docker` group you are effectively granting `gitlab-runner` full root permissions.
-For more information please checkout [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
+> **Note:**
+* By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
+For more information please read [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
-## 2. Use docker-in-docker executor
+### Use docker-in-docker executor
-The second approach is to use the special Docker image with all tools installed
+The second approach is to use the special docker-in-docker (dind)
+[Docker image](https://hub.docker.com/_/docker/) with all tools installed
(`docker` and `docker-compose`) and run the build script in context of that
image in privileged mode.
-In order to do that follow the steps:
+In order to do that, follow the steps:
1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
@@ -87,9 +90,9 @@ In order to do that follow the steps:
mode:
```bash
- sudo gitlab-runner register -n \
+ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
- --token RUNNER_TOKEN \
+ --registration-token REGISTRATION_TOKEN \
--executor docker \
--description "My Docker Runner" \
--docker-image "docker:latest" \
@@ -119,11 +122,7 @@ In order to do that follow the steps:
Insecure = false
```
- If you want to use the Shared Runners available on your GitLab CE/EE
- installation in order to build Docker images, then make sure that your
- Shared Runners configuration has the `privileged` mode set to `true`.
-
-1. You can now use `docker` from build script:
+1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
```yaml
image: docker:latest
@@ -141,14 +140,177 @@ In order to do that follow the steps:
- docker run my-docker-image /script/to/run/tests
```
-1. However, by enabling `--docker-privileged` you are effectively disabling all
- the security mechanisms of containers and exposing your host to privilege
- escalation which can lead to container breakout.
-
- For more information, check out the official Docker documentation on
- [Runtime privilege and Linux capabilities][docker-cap].
+Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
+* By enabling `--docker-privileged`, you are effectively disabling all of
+the security mechanisms of containers and exposing your host to privilege
+escalation which can lead to container breakout. For more information, check out the official Docker documentation on
+[Runtime privilege and Linux capabilities][docker-cap].
+* Using docker-in-docker, each build is in a clean environment without the past
+history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers.
+* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form
+offered.
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
+### Use Docker socket binding
+
+The third approach is to bind-mount `/var/run/docker.sock` into the container so that docker is available in the context of that image.
+
+In order to do that, follow the steps:
+
+1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation).
+
+1. Register GitLab Runner from the command line to use `docker` and share `/var/run/docker.sock`:
+
+ ```bash
+ sudo gitlab-ci-multi-runner register -n \
+ --url https://gitlab.com/ci \
+ --registration-token REGISTRATION_TOKEN \
+ --executor docker \
+ --description "My Docker Runner" \
+ --docker-image "docker:latest" \
+ --docker-volumes /var/run/docker.sock:/var/run/docker.sock
+ ```
+
+ The above command will register a new Runner to use the special
+ `docker:latest` image which is provided by Docker. **Notice that it's using
+ the Docker daemon of the Runner itself, and any containers spawned by docker commands will be siblings of the Runner rather than children of the runner.** This may have complications and limitations that are unsuitable for your workflow.
+
+ The above command will create a `config.toml` entry similar to this:
+
+ ```
+ [[runners]]
+ url = "https://gitlab.com/ci"
+ token = REGISTRATION_TOKEN
+ executor = "docker"
+ [runners.docker]
+ tls_verify = false
+ image = "docker:latest"
+ privileged = false
+ disable_cache = false
+ volumes = ["/var/run/docker.sock", "/cache"]
+ [runners.cache]
+ Insecure = false
+ ```
+
+1. You can now use `docker` in the build script (note that you don't need to include the `docker:dind` service as when using the Docker in Docker executor):
+
+ ```yaml
+ image: docker:latest
+
+ before_script:
+ - docker info
+
+ build:
+ stage: build
+ script:
+ - docker build -t my-docker-image .
+ - docker run my-docker-image /script/to/run/tests
+ ```
+
+While the above method avoids using Docker in privileged mode, you should be aware of the following implications:
+* By sharing the docker daemon, you are effectively disabling all
+the security mechanisms of containers and exposing your host to privilege
+escalation which can lead to container breakout. For example, if a project
+ran `docker rm -f $(docker ps -a -q)` it would remove the GitLab Runner
+containers.
+* Concurrent builds may not work; if your tests
+create containers with specific names, they may conflict with each other.
+* Sharing files and directories from the source repo into containers may not
+work as expected since volume mounting is done in the context of the host
+machine, not the build container.
+e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
+
+## Using the GitLab Container Registry
+
+> **Note:**
+This feature requires GitLab 8.8 and GitLab Runner 1.2.
+
+Once you've built a Docker image, you can push it up to the built-in [GitLab Container Registry](../../container_registry/README.md). For example, if you're using
+docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
+
+
+```yaml
+ build:
+ image: docker:latest
+ services:
+ - docker:dind
+ stage: build
+ script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+ - docker build -t registry.example.com/group/project:latest .
+ - docker push registry.example.com/group/project:latest
+```
+
+You have to use the credentials of the special `gitlab-ci-token` user with its
+password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
+to your project. This allows you to automate building and deployment of your
+Docker images.
+
+Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
+including two tests that run in parallel. The build is stored in the container
+registry and used by subsequent stages, downloading the image
+when needed. Changes to `master` also get tagged as `latest` and deployed using
+an application-specific deploy script:
+
+```yaml
+image: docker:latest
+services:
+- docker:dind
+
+stages:
+- build
+- test
+- release
+- deploy
+
+variables:
+ CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_BUILD_REF_NAME
+ CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project:latest
+
+before_script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+
+build:
+ stage: build
+ script:
+ - docker build --pull -t $CONTAINER_TEST_IMAGE .
+ - docker push $CONTAINER_TEST_IMAGE
+
+test1:
+ stage: test
+ script:
+ - docker pull $CONTAINER_TEST_IMAGE
+ - docker run $CONTAINER_TEST_IMAGE /script/to/run/tests
+
+test2:
+ stage: test
+ script:
+ - docker pull $CONTAINER_TEST_IMAGE
+ - docker run $CONTAINER_TEST_IMAGE /script/to/run/another/test
+
+release-image:
+ stage: release
+ script:
+ - docker pull $CONTAINER_TEST_IMAGE
+ - docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
+ - docker push $CONTAINER_RELEASE_IMAGE
+ only:
+ - master
+
+deploy:
+ stage: deploy
+ script:
+ - ./deploy.sh
+ only:
+ - master
+```
+
+Some things you should be aware of when using the Container Registry:
+* You must log in to the container registry before running commands. Putting this in `before_script` will run it before each build job.
+* Using `docker build --pull` makes sure that Docker fetches any changes to base images before building just in case your cache is stale. It takes slightly longer, but means you don’t get stuck without security patches to base images.
+* Doing an explicit `docker pull` before each `docker run` makes sure to fetch the latest image that was just built. This is especially important if you are using multiple runners that cache images locally. Using the git SHA in your image tag makes this less necessary since each build will be unique and you shouldn't ever have a stale image, but it's still possible if you re-build a given commit after a dependency has changed.
+* You don't want to build directly to `latest` in case there are multiple builds happening simultaneously.
+
[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 56ac2195c49..a849905ac6b 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -23,7 +23,7 @@ To use GitLab Runner with docker you need to register a new runner to use the
`docker` executor:
```bash
-gitlab-runner register \
+gitlab-ci-multi-runner register \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "docker-ruby-2.1" \
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
new file mode 100644
index 00000000000..d85b8a34ced
--- /dev/null
+++ b/doc/ci/environments.md
@@ -0,0 +1,58 @@
+# Introduction to environments and deployments
+
+>**Note:**
+Introduced in GitLab 8.9.
+
+## Environments
+
+Environments are places where code gets deployed, such as staging or production.
+CI/CD [Pipelines] usually have one or more [jobs] that deploy to an environment.
+Defining environments in a project's `.gitlab-ci.yml` lets developers track
+[deployments] to these environments.
+
+## Deployments
+
+Deployments are created when [jobs] deploy versions of code to [environments].
+
+## Defining environments
+
+You can create and delete environments manually in the web interface, but we
+recommend that you define your environments in `.gitlab-ci.yml` first, which
+will automatically create environments for you after the first deploy.
+
+The `environment` is just a hint for GitLab that this job actually deploys to
+this environment. Each time the job succeeds, a deployment is recorded,
+remembering the git SHA and environment.
+
+Add something like this to your `.gitlab-ci.yml`:
+```
+production:
+ stage: deploy
+ script: dpl...
+ environment: production
+```
+
+See full [documentation](yaml/README.md#environment).
+
+## Seeing environment status
+
+You can find the environment list under **Pipelines > Environments** for your
+project. You'll see the git SHA and date of the last deployment to each
+environment defined.
+
+>**Note:**
+Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
+show up in the environments and deployments lists.
+
+## Seeing deployment history
+
+Clicking on an environment will show the history of deployments.
+
+>**Note:**
+Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
+show up in the environments and deployments lists.
+
+[Pipelines]: pipelines.md
+[jobs]: yaml/README.md#jobs
+[environments]: #environments
+[deployments]: #deployments
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 61294be599d..27bc21c2922 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -4,7 +4,8 @@
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
-- [Using `dpl` as deployment tool](../deployment/README.md)
+- [Test a Scala application](test-scala-application.md)
+- [Using `dpl` as deployment tool](deployment/README.md)
- Help your favorite programming language and GitLab by sending a merge request
with a guide for that language.
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index 26953014502..17e1c64bb8a 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -263,10 +263,10 @@ terminal execute:
```bash
# Check using docker executor
-gitlab-runner exec docker test:app
+gitlab-ci-multi-runner exec docker test:app
# Check using shell executor
-gitlab-runner exec shell test:app
+gitlab-ci-multi-runner exec shell test:app
```
## Example project
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
new file mode 100644
index 00000000000..7412fdbbc78
--- /dev/null
+++ b/doc/ci/examples/test-scala-application.md
@@ -0,0 +1,47 @@
+## Test a Scala application
+
+This example demonstrates the integration of Gitlab CI with Scala
+applications using SBT. Checkout the example
+[project](https://gitlab.com/gitlab-examples/scala-sbt) and
+[build status](https://gitlab.com/gitlab-examples/scala-sbt/builds).
+
+### Add `.gitlab-ci.yml` file to project
+
+The following `.gitlab-ci.yml` should be added in the root of your
+repository to trigger CI:
+
+``` yaml
+image: java:8
+
+before_script:
+ - apt-get update -y
+ - apt-get install apt-transport-https -y
+ # Install SBT
+ - echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
+ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
+ - apt-get update -y
+ - apt-get install sbt -y
+ - sbt sbt-version
+
+test:
+ script:
+ - sbt clean coverage test coverageReport
+```
+
+The `before_script` installs [SBT](http://www.scala-sbt.org/) and
+displays the version that is being used. The `test` stage executes SBT
+to compile and test the project.
+[scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT
+plugin to measure test coverage.
+
+You can use other versions of Scala and SBT by defining them in
+`build.sbt`.
+
+### Display test coverage in build
+
+Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
+**Settings > Edit Project > Test coverage parsing** project setting to
+retrieve the test coverage rate from the build trace and have it
+displayed with your builds.
+
+**Builds** must be enabled for this option to appear.
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
new file mode 100644
index 00000000000..48a9f994759
--- /dev/null
+++ b/doc/ci/pipelines.md
@@ -0,0 +1,38 @@
+# Introduction to pipelines and builds
+
+>**Note:**
+Introduced in GitLab 8.8.
+
+## Pipelines
+
+A pipeline is a group of [builds] that get executed in [stages] (batches). All
+of the builds in a stage are executed in parallel (if there are enough
+concurrent [runners]), and if they all succeed, the pipeline moves on to the
+next stage. If one of the builds fails, the next stage is not (usually)
+executed.
+
+## Builds
+
+Builds are individual runs of [jobs]. Not to be confused with a `build` job or
+`build` stage.
+
+## Defining pipelines
+
+Pipelines are defined in `.gitlab-ci.yml` by specifying [jobs] that run in
+[stages].
+
+See full [documentation](yaml/README.md#jobs).
+
+## Seeing pipeline status
+
+You can find the current and historical pipeline runs under **Pipelines** for your
+project.
+
+## Seeing build status
+
+Clicking on a pipeline will show the builds that were run for that pipeline.
+
+[builds]: #builds
+[jobs]: yaml/README.md#jobs
+[stages]: yaml/README.md#stages
+[runners]: runners/README.md
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 386b8e29fcf..7fa1a478f34 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -4,41 +4,41 @@
is fully integrated into GitLab itself and is [enabled] by default on all
projects.
-The TL;DR version of how GitLab CI works is the following.
-
----
-
GitLab offers a [continuous integration][ci] service. If you
[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
and configure your GitLab project to use a [Runner], then each merge request or
-push triggers a build.
+push triggers your CI [pipeline].
-The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it
-runs three [stages]: `build`, `test`, and `deploy`.
+The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs
+a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to
+use all three stages; stages with no jobs are simply ignored.
If everything runs OK (no non-zero return values), you'll get a nice green
checkmark associated with the pushed commit or merge request. This makes it
-easy to see whether a merge request will cause any of the tests to fail before
+easy to see whether a merge request caused any of the tests to fail before
you even look at the code.
-Most projects only use GitLab's CI service to run the test suite so that
+Most projects use GitLab's CI service to run the test suite so that
developers get immediate feedback if they broke something.
+There's a growing trend to use continuous delivery and continuous deployment to
+automatically deploy tested code to staging and production environments.
+
So in brief, the steps needed to have a working CI can be summed up to:
1. Add `.gitlab-ci.yml` to the root directory of your repository
1. Configure a Runner
-From there on, on every push to your Git repository, the build will be
-automagically started by the Runner and will appear under the project's
-`/builds` page.
+From there on, on every push to your Git repository, the Runner will
+automagically start the pipeline and the pipeline will appear under the
+project's `/pipelines` page.
---
This guide assumes that you:
- have a working GitLab instance of version 8.0 or higher or are using
- [GitLab.com](https://gitlab.com/users/sign_in)
+ [GitLab.com](https://gitlab.com)
- have a project in GitLab that you would like to use CI for
Let's break it down to pieces and work on solving the GitLab CI puzzle.
@@ -57,15 +57,14 @@ On any push to your repository, GitLab will look for the `.gitlab-ci.yml`
file and start builds on _Runners_ according to the contents of the file,
for that commit.
-Because `.gitlab-ci.yml` is in the repository, it is version controlled,
-old versions still build successfully, forks can easily make use of CI,
-branches can have separate builds and you have a single source of truth for CI.
-You can read more about the reasons why we are using `.gitlab-ci.yml`
-[in our blog about it][blog-ci].
+Because `.gitlab-ci.yml` is in the repository and is version controlled, old
+versions still build successfully, forks can easily make use of CI, branches can
+have different pipelines and jobs, and you have a single source of truth for CI.
+You can read more about the reasons why we are using `.gitlab-ci.yml` [in our
+blog about it][blog-ci].
**Note:** `.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file
-so you have to pay extra attention to the indentation. Always use spaces, not
-tabs.
+so you have to pay extra attention to indentation. Always use spaces, not tabs.
### Creating a simple `.gitlab-ci.yml` file
@@ -108,7 +107,7 @@ If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
the link under **Settings > CI settings** in your project.
-For more information and a complete `.gitlab-ci.yml` syntax, please check
+For more information and a complete `.gitlab-ci.yml` syntax, please read
[the documentation on .gitlab-ci.yml](../yaml/README.md).
### Push `.gitlab-ci.yml` to GitLab
@@ -122,7 +121,8 @@ git commit -m "Add .gitlab-ci.yml"
git push origin master
```
-Now if you go to the **Builds** page you will see that the builds are pending.
+Now if you go to the **Pipelines** page you will see that the pipeline is
+pending.
You can also go to the **Commits** page and notice the little clock icon next
to the commit SHA.
@@ -138,15 +138,14 @@ Notice that there are two jobs pending which are named after what we wrote in
`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured
yet for these builds.
-The next step is to configure a Runner so that it picks the pending jobs.
+The next step is to configure a Runner so that it picks the pending builds.
## Configuring a Runner
-In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`.
-A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker
-container or even a cluster of containers. GitLab and the Runners communicate
-through an API, so the only needed requirement is that the machine on which the
-Runner is configured to have Internet access.
+In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. A Runner
+can be a virtual machine, a VPS, a bare-metal machine, a docker container or
+even a cluster of containers. GitLab and the Runners communicate through an API,
+so the only requirement is that the Runner's machine has Internet access.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
@@ -188,12 +187,16 @@ To enable **Shared Runners** you have to go to your project's
[Read more on Shared Runners](../runners/README.md).
-## Seeing the status of your build
+## Seeing the status of your pipeline and builds
After configuring the Runner successfully, you should see the status of your
last commit change from _pending_ to either _running_, _success_ or _failed_.
-You can view all builds, by going to the **Builds** page in your project.
+You can view all pipelines by going to the **Pipelines** page in your project.
+
+![Commit status](img/pipelines_status.png)
+
+Or you can view all builds, by going to the **Pipelines > Builds** page.
![Commit status](img/builds_status.png)
@@ -238,3 +241,4 @@ CI with various languages.
[runner]: ../runners/README.md
[enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages
+[pipeline]: ../pipelines.md
diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png
new file mode 100644
index 00000000000..6bc97bb739c
--- /dev/null
+++ b/doc/ci/quick_start/img/pipelines_status.png
Binary files differ
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index b42d7a62ebc..ddebd987650 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -63,10 +63,10 @@ instance.
Now simply register the runner as any runner:
```
-sudo gitlab-runner register
+sudo gitlab-ci-multi-runner register
```
-Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the
+Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the
`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared runners to
disabled.
@@ -93,9 +93,15 @@ setup a specific runner for this project.
To register the runner, run the command below and follow instructions:
```
-sudo gitlab-runner register
+sudo gitlab-ci-multi-runner register
```
+### Lock a specific runner from being enabled for other projects
+
+You can configure a runner to assign it exclusively to a project. When a
+runner is locked this way, it can no longer be enabled for other projects.
+This setting is available on each runner in *Project Settings* > *Runners*.
+
### Making an existing Shared Runner Specific
If you are an admin on your GitLab instance,
@@ -128,7 +134,7 @@ the appropriate dependencies to run Rails test suites.
### Prevent runner with tags from picking jobs without tags
You can configure a runner to prevent it from picking jobs with tags when
-the runnner does not have tags assigned. This setting is available on each
+the runner does not have tags assigned. This setting is available on each
runner in *Project Settings* > *Runners*.
### Be careful with sensitive information
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 70fb81492d6..137b080a8f7 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -34,6 +34,7 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_BUILD_ID** | all | The unique id of the current build that GitLab CI uses internally |
| **CI_BUILD_REPO** | all | The URL to clone the Git repository |
| **CI_BUILD_TRIGGERED** | 0.5 | The flag to indicate that build was [triggered] |
+| **CI_BUILD_TOKEN** | 1.2 | Token used for authenticating with the GitLab Container Registry |
| **CI_PROJECT_ID** | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_DIR** | all | The full path where the repository is cloned and where the build is ran |
@@ -50,6 +51,7 @@ export CI_BUILD_TAG="1.0.0"
export CI_BUILD_NAME="spec:other"
export CI_BUILD_STAGE="test"
export CI_BUILD_TRIGGERED="true"
+export CI_BUILD_TOKEN="abcde-1234ABCD5678ef"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
export CI_PROJECT_ID="34"
export CI_SERVER="yes"
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 0707555e393..1892acda29b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -13,30 +13,34 @@ If you want a quick introduction to GitLab CI, follow our
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [.gitlab-ci.yml](#gitlab-ci-yml)
- - [image and services](#image-and-services)
- - [before_script](#before_script)
- - [after_script](#after_script)
- - [stages](#stages)
- - [types](#types)
- - [variables](#variables)
- - [cache](#cache)
- - [cache:key](#cache-key)
+ - [image and services](#image-and-services)
+ - [before_script](#before_script)
+ - [after_script](#after_script)
+ - [stages](#stages)
+ - [types](#types)
+ - [variables](#variables)
+ - [cache](#cache)
+ - [cache:key](#cache-key)
- [Jobs](#jobs)
- - [script](#script)
- - [stage](#stage)
- - [job variables](#job-variables)
- - [only and except](#only-and-except)
- - [tags](#tags)
- - [when](#when)
- - [artifacts](#artifacts)
- - [artifacts:name](#artifacts-name)
- - [artifacts:when](#artifacts-when)
- - [dependencies](#dependencies)
- - [before_script and after_script](#before_script-and-after_script)
+ - [script](#script)
+ - [stage](#stage)
+ - [only and except](#only-and-except)
+ - [job variables](#job-variables)
+ - [tags](#tags)
+ - [when](#when)
+ - [environment](#environment)
+ - [artifacts](#artifacts)
+ - [artifacts:name](#artifactsname)
+ - [artifacts:when](#artifactswhen)
+ - [artifacts:expire_in](#artifactsexpire_in)
+ - [dependencies](#dependencies)
+ - [before_script and after_script](#before_script-and-after_script)
+- [Git Strategy](#git-strategy)
+- [Shallow cloning](#shallow-cloning)
- [Hidden jobs](#hidden-jobs)
- [Special YAML features](#special-yaml-features)
- - [Anchors](#anchors)
-- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
+ - [Anchors](#anchors)
+- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ciyml)
- [Skipping builds](#skipping-builds)
- [Examples](#examples)
@@ -52,7 +56,7 @@ of your repository and contains definitions of how your project should be built.
The YAML file defines a set of jobs with constraints stating when they should
be run. The jobs are defined as top-level elements with a name and always have
-to contain the `script` clause:
+to contain at least the `script` clause:
```yaml
job1:
@@ -163,9 +167,9 @@ stages:
There are also two edge cases worth mentioning:
-1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`,
+1. If no `stages` are defined in `.gitlab-ci.yml`, then by default the `build`,
`test` and `deploy` are allowed to be used as job's stage by default.
-2. If a job doesn't specify `stage`, the job is assigned the `test` stage.
+2. If a job doesn't specify a `stage`, the job is assigned the `test` stage.
### types
@@ -176,9 +180,9 @@ Alias for [stages](#stages).
>**Note:**
Introduced in GitLab Runner v0.5.0.
-GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
-environment. The variables are stored in the git repository and are meant to
-store non-sensitive project configuration, for example:
+GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
+build environment. The variables are stored in the git repository and are meant
+to store non-sensitive project configuration, for example:
```yaml
variables:
@@ -251,8 +255,8 @@ rspec:
- binaries/
```
-The cache is provided on best effort basis, so don't expect that cache will be
-always present. For implementation details please check GitLab Runner.
+The cache is provided on a best-effort basis, so don't expect that the cache
+will be always present. For implementation details, please check GitLab Runner.
#### cache:key
@@ -353,6 +357,7 @@ job_name:
| cache | no | Define list of files that should be cached between subsequent runs |
| before_script | no | Override a set of commands that are executed before build |
| after_script | no | Override a set of commands that are executed after build |
+| environment | no | Defines a name of environment to which deployment is done by this build |
### script
@@ -476,10 +481,10 @@ failure.
`when` can be set to one of the following values:
1. `on_success` - execute build only when all builds from prior stages
- succeeded. This is the default.
+ succeed. This is the default.
1. `on_failure` - execute build only when at least one build from prior stages
- failed.
-1. `always` - execute build despite the status of builds from prior stages.
+ fails.
+1. `always` - execute build regardless of the status of builds from prior stages.
For example:
@@ -524,6 +529,36 @@ The above script will:
1. Execute `cleanup_build_job` only when `build_job` fails
2. Always execute `cleanup_job` as the last step in pipeline.
+### environment
+
+>**Note:**
+Introduced in GitLab 8.9.
+
+`environment` is used to define that a job deploys to a specific environment.
+This allows easy tracking of all deployments to your environments straight from
+GitLab.
+
+If `environment` is specified and no environment under that name exists, a new
+one will be created automatically.
+
+The `environment` name must contain only letters, digits, '-' and '_'. Common
+names are `qa`, `staging`, and `production`, but you can use whatever name works
+with your workflow.
+
+---
+
+**Example configurations**
+
+```
+deploy to production:
+ stage: deploy
+ script: git push production HEAD:master
+ environment: production
+```
+
+The `deploy to production` job will be marked as doing deployment to
+`production` environment.
+
### artifacts
>**Notes:**
@@ -531,10 +566,10 @@ The above script will:
> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
> - Windows support was added in GitLab Runner v.1.0.0.
> - Currently not all executors are supported.
-> - Build artifacts are only collected for successful builds.
+> - Build artifacts are only collected for successful builds by default.
-`artifacts` is used to specify list of files and directories which should be
-attached to build after success. To pass artifacts between different builds,
+`artifacts` is used to specify a list of files and directories which should be
+attached to the build after success. To pass artifacts between different builds,
see [dependencies](#dependencies).
Below are some examples.
@@ -662,9 +697,9 @@ failure.
`artifacts:when` can be set to one of the following values:
-1. `on_success` - upload artifacts only when build succeeds. This is the default
-1. `on_failure` - upload artifacts only when build fails
-1. `always` - upload artifacts despite the build status
+1. `on_success` - upload artifacts only when the build succeeds. This is the default.
+1. `on_failure` - upload artifacts only when the build fails.
+1. `always` - upload artifacts regardless of the build status.
---
@@ -678,6 +713,42 @@ job:
when: on_failure
```
+#### artifacts:expire_in
+
+>**Note:**
+Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
+
+`artifacts:expire_in` is used to delete uploaded artifacts after the specified
+time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
+to specify how long artifacts should live before they expire, counting from the
+time they are uploaded and stored on GitLab.
+
+You can use the **Keep** button on the build page to override expiration and
+keep artifacts forever.
+
+After expiry, artifacts are actually deleted hourly by default (via a cron job),
+but they are not accessible after expiry.
+
+The value of `expire_in` is an elapsed time. Examples of parseable values:
+- '3 mins 4 sec'
+- '2 hrs 20 min'
+- '2h20min'
+- '6 mos 1 day'
+- '47 yrs 6 mos and 4d'
+- '3 weeks and 2 days'
+
+---
+
+**Example configurations**
+
+To expire artifacts 1 week after being uploaded:
+
+```yaml
+job:
+ artifacts:
+ expire_in: 1 week
+```
+
### dependencies
>**Note:**
@@ -752,6 +823,61 @@ job:
- execute this after my script
```
+## Git Strategy
+
+>**Note:**
+Introduced in GitLab 8.9 as an experimental feature. May change in future
+releases or be removed completely.
+
+You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
+is slower, but makes sure you have a clean directory before every build. `fetch`
+is faster. `GIT_STRATEGY` can be specified in the global `variables` section or
+in the `variables` section for individual jobs. If it's not specified, then the
+default from project settings will be used.
+
+```
+variables:
+ GIT_STRATEGY: clone
+```
+
+or
+
+```
+variables:
+ GIT_STRATEGY: fetch
+```
+
+## Shallow cloning
+
+>**Note:**
+Introduced in GitLab 8.9 as an experimental feature. May change in future
+releases or be removed completely.
+
+You can specify the depth of fetching and cloning using `GIT_DEPTH`. This allows
+shallow cloning of the repository which can significantly speed up cloning for
+repositories with a large number of commits or old, large binaries. The value is
+passed to `git fetch` and `git clone`.
+
+>**Note:**
+If you use a depth of 1 and have a queue of builds or retry
+builds, jobs may fail.
+
+Since Git fetching and cloning is based on a ref, such as a branch name, runners
+can't clone a specific commit SHA. If there are multiple builds in the queue, or
+you are retrying an old build, the commit to be tested needs to be within the
+git history that is cloned. Setting too small a value for `GIT_DEPTH` can make
+it impossible to run these old commits. You will see `unresolved reference` in
+build logs. You should then reconsider changing `GIT_DEPTH` to a higher value.
+
+Builds that rely on `git describe` may not work correctly when `GIT_DEPTH` is
+set since only part of the git history is present.
+
+To fetch or clone only the last 3 commits:
+```
+variables:
+ GIT_DEPTH: "3"
+```
+
## Hidden jobs
>**Note:**
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 4df24ef13cc..1b465434498 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -79,27 +79,8 @@ delete them.
This feature requires GitLab 8.8 and GitLab Runner 1.2.
Make sure that your GitLab Runner is configured to allow building docker images.
-You have to check the [Using Docker Build documentation](../../ci/docker/using_docker_build.md).
-
-You can use [docker:dind](https://hub.docker.com/_/docker/) to build your images,
-and this is how `.gitlab-ci.yml` should look like:
-
-```
- build_image:
- image: docker:git
- services:
- - docker:dind
- stage: build
- script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
- - docker build -t registry.example.com/group/project:latest .
- - docker push registry.example.com/group/project:latest
-```
-
-You have to use the credentials of the special `gitlab-ci-token` user with its
-password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
-to your project. This allows you to automated building and deployment of your
-Docker images.
+You have to check the [Using Docker Build documentation](../ci/docker/using_docker_build.md).
+Then see the CI documentation on [Using the GitLab Container Registry](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index f5d97179f8a..975bb82c37d 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -183,6 +183,62 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
(`workflow/lfs/lfs_administration.md`).
+## Configuration documentation for source and Omnibus installations
+
+GitLab currently officially supports two installation methods: installations
+from source and Omnibus packages installations.
+
+Whenever there is a setting that is configurable for both installation methods,
+prefer to document it in the CE docs to avoid duplication.
+
+Configuration settings include:
+
+- settings that touch configuration files in `config/`
+- NGINX settings and settings in `lib/support/` in general
+
+When there is a list of steps to perform, usually that entails editing the
+configuration file and reconfiguring/restarting GitLab. In such case, follow
+the style below as a guide:
+
+````
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ external_url "https://gitlab.example.com"
+ ```
+
+1. Save the file and [reconfigure] GitLab for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ gitlab:
+ host: "gitlab.example.com"
+ ```
+
+1. Save the file and [restart] GitLab for the changes to take effect.
+
+
+[reconfigure]: path/to/administration/gitlab_restart.md#omnibus-gitlab-reconfigure
+[restart]: path/to/administration/gitlab_restart.md#installations-from-source
+````
+
+In this case:
+
+- before each step list the installation method is declared in bold
+- three dashes (`---`) are used to create an horizontal line and separate the
+ two methods
+- the code blocks are indented one or more spaces under the list item to render
+ correctly
+- different highlighting languages are used for each config in the code block
+- the [references](#references) guide is used for reconfigure/restart
+
## API
Here is a list of must-have items. Use them in the exact order that appears
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 9168c70945a..c2272ab0a2b 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -15,8 +15,8 @@ instrument code:
* `instrument_instance_method`: instruments a single instance method.
* `instrument_class_hierarchy`: given a Class this method will recursively
instrument all sub-classes (both class and instance methods).
-* `instrument_methods`: instruments all public class methods of a Module.
-* `instrument_instance_methods`: instruments all public instance methods of a
+* `instrument_methods`: instruments all public and private class methods of a Module.
+* `instrument_instance_methods`: instruments all public and private instance methods of a
Module.
To remove the need for typing the full `Gitlab::Metrics::Instrumentation`
@@ -94,22 +94,8 @@ Visibility: public
Number of lines: 21
def #{name}(#{args_signature})
- trans = Gitlab::Metrics::Instrumentation.transaction
-
- if trans
- start = Time.now
- retval = super
- duration = (Time.now - start) * 1000.0
-
- if duration >= Gitlab::Metrics.method_call_threshold
- trans.increment(:method_duration, duration)
-
- trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
- { duration: 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/doc/intro/README.md b/doc/intro/README.md
index 382d10aaf40..1850031eb26 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -12,7 +12,7 @@ Create projects and groups.
Create issues, labels, milestones, cast your vote, and review issues.
- [Create a new issue](../gitlab-basics/create-issue.md)
-- [Assign labels to issues](../workflow/labels.md)
+- [Assign labels to issues](../user/project/labels.md)
- [Use milestones as an overview of your project's tracker](../workflow/milestones.md)
- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index b76ce31cbad..963b35de3a0 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -28,6 +28,7 @@ documentation](../workflow/add-user/add-user.md).
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
+| See environments | | ✓ | ✓ | ✓ | ✓ |
| Manage merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
@@ -40,6 +41,7 @@ documentation](../workflow/add-user/add-user.md).
| Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
+| Create new environments | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
@@ -52,6 +54,7 @@ documentation](../workflow/add-user/add-user.md).
| Manage runners | | | | ✓ | ✓ |
| Manage build triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
+| Delete environments | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md
new file mode 100644
index 00000000000..2f9f36f962e
--- /dev/null
+++ b/doc/project_services/emails_on_push.md
@@ -0,0 +1,17 @@
+## Enabling emails on push
+
+To receive email notifications for every change that is pushed to the project, visit
+your project's **Settings > Services > Emails on push** and activate the service.
+
+In the _Recipients_ area, provide a list of emails separated by commas.
+
+You can configure any of the following settings depending on your preference.
+
++ **Push events** - Email will be triggered when a push event is recieved
++ **Tag push events** - Email will be triggered when a tag is created and pushed
++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
+
+---
+
+![Email on push service settings](img/emails_on_push_service.png)
diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png
new file mode 100644
index 00000000000..cd6f79ad1eb
--- /dev/null
+++ b/doc/project_services/img/emails_on_push_service.png
Binary files differ
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index a5af620d9be..f81a035f70b 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -33,7 +33,7 @@ further configuration instructions and details. Contributions are welcome.
| Campfire | Simple web-based real-time group chat |
| Custom Issue Tracker | Custom issue tracker |
| Drone CI | Continuous Integration platform built on Docker, written in Go |
-| Emails on push | Email the commits and diff of each push to a list of recipients |
+| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index bb463d43a7c..cb66ef920bb 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -45,7 +45,7 @@ sudo -u git -H git checkout 8-7-stable-ee
```bash
cd /home/git/gitlab-shell
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --tags
sudo -u git -H git checkout v2.7.2
```
diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png
new file mode 100644
index 00000000000..e32a35f7cda
--- /dev/null
+++ b/doc/user/project/img/labels_assign_label_in_new_issue.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_sidebar.png b/doc/user/project/img/labels_assign_label_sidebar.png
new file mode 100644
index 00000000000..799443af889
--- /dev/null
+++ b/doc/user/project/img/labels_assign_label_sidebar.png
Binary files differ
diff --git a/doc/user/project/img/labels_assign_label_sidebar_saved.png b/doc/user/project/img/labels_assign_label_sidebar_saved.png
new file mode 100644
index 00000000000..e7d8d69e60e
--- /dev/null
+++ b/doc/user/project/img/labels_assign_label_sidebar_saved.png
Binary files differ
diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png
new file mode 100644
index 00000000000..ee0c9f889ad
--- /dev/null
+++ b/doc/user/project/img/labels_default.png
Binary files differ
diff --git a/doc/user/project/img/labels_description_tooltip.png b/doc/user/project/img/labels_description_tooltip.png
new file mode 100644
index 00000000000..0d1e3e091fb
--- /dev/null
+++ b/doc/user/project/img/labels_description_tooltip.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png
new file mode 100644
index 00000000000..ed622be2d93
--- /dev/null
+++ b/doc/user/project/img/labels_filter.png
Binary files differ
diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png
new file mode 100644
index 00000000000..c5a9e20919b
--- /dev/null
+++ b/doc/user/project/img/labels_filter_by_priority.png
Binary files differ
diff --git a/doc/user/project/img/labels_generate.png b/doc/user/project/img/labels_generate.png
new file mode 100644
index 00000000000..9579be4e231
--- /dev/null
+++ b/doc/user/project/img/labels_generate.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png
new file mode 100644
index 00000000000..a916d3dceb5
--- /dev/null
+++ b/doc/user/project/img/labels_new_label.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label_on_the_fly.png b/doc/user/project/img/labels_new_label_on_the_fly.png
new file mode 100644
index 00000000000..80cc434239e
--- /dev/null
+++ b/doc/user/project/img/labels_new_label_on_the_fly.png
Binary files differ
diff --git a/doc/user/project/img/labels_new_label_on_the_fly_create.png b/doc/user/project/img/labels_new_label_on_the_fly_create.png
new file mode 100644
index 00000000000..c41090945eb
--- /dev/null
+++ b/doc/user/project/img/labels_new_label_on_the_fly_create.png
Binary files differ
diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png
new file mode 100644
index 00000000000..8dfe72cf826
--- /dev/null
+++ b/doc/user/project/img/labels_prioritize.png
Binary files differ
diff --git a/doc/user/project/img/labels_subscribe.png b/doc/user/project/img/labels_subscribe.png
new file mode 100644
index 00000000000..ea3db2bc0cf
--- /dev/null
+++ b/doc/user/project/img/labels_subscribe.png
Binary files differ
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
new file mode 100644
index 00000000000..4258185b7d0
--- /dev/null
+++ b/doc/user/project/labels.md
@@ -0,0 +1,147 @@
+# Labels
+
+Labels provide an easy way to categorize the issues or merge requests based on
+descriptive titles like `bug`, `documentation` or any other text you feel like
+it. They can have different colors, a description, and are visible throughout
+the issue tracker or inside each issue individually.
+
+With labels, you can navigate the issue tracker and filter any bloated
+information to visualize only the issues you are interested in. Let's see how
+that works.
+
+## Create new labels
+
+>**Note:**
+A permission level of `Developer` or higher is required in order to manage
+labels.
+
+Head over a single project and navigate to **Issues > Labels**.
+
+The first time you visit this page, you'll notice that there are no labels
+created yet.
+
+![Generate new labels](img/labels_generate.png)
+
+---
+
+You can skip that and create a new label or click that link and GitLab will
+generate a set of predefined labels for you. There 8 default generated labels
+in total and you can see them in the screenshot below.
+
+![Default generated labels](img/labels_default.png)
+
+---
+
+You can see that from the labels page you can have an overview of the number of
+issues and merge requests assigned to each label.
+
+Creating a new label from scratch is as easy as pressing the **New label**
+button. From there on you can choose the name, give it an optional description,
+a color and you are set.
+
+When you are ready press the **Create label** button to create the new label.
+
+![New label](img/labels_new_label.png)
+
+## Prioritize labels
+
+>**Notes:**
+ - This feature was introduced in GitLab 8.9.
+ - Priority sorting is based on the highest priority label only. This might
+ change in the future, follow the discussion in
+ https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
+
+Prioritized labels are like any other label, but sorted by priority. This allows
+you to sort issues and merge requests by priority.
+
+To prioritize labels, navigate to your project's **Issues > Labels** and click
+on the star icon next to them to put them in the priority list. Click on the
+star icon again to remove them from the list.
+
+From there, you can drag them around to set the desired priority. Priority is
+set from high to low with an ascending order. Labels with no priority, count as
+having their priority set to null.
+
+![Prioritize labels](img/labels_prioritize.png)
+
+Now that you have labels prioritized, you can use the 'Priority' filter in the
+issues or merge requests tracker. Those with the highest priority label, will
+appear on top.
+
+![Filter labels by priority](img/labels_filter_by_priority.png)
+
+## Subscribe to labels
+
+If you don’t want to miss issues or merge requests that are important to you,
+simply subscribe to a label. You’ll get notified whenever the label gets added
+to an issue or merge request, making sure you don’t miss a thing.
+
+Go to your project's **Issues > Labels** area, find the label(s) you want to
+subscribe to and click on the eye icon. Click again to unsubscribe.
+
+![Subscribe to labels](img/labels_subscribe.png)
+
+If you work on a large or popular project, try subscribing only to the labels
+that are relevant to you. You’ll notice it’ll be much easier to focus on what’s
+important.
+
+## Create a new label right from the issue tracker
+
+>**Note:**
+This feature was introduced in GitLab 8.6.
+
+There are times when you are already in the issue tracker searching for a
+label, only to realize it doesn't exist. Instead of going to the **Labels**
+page and being distracted from your original purpose, you can create new
+labels on the fly.
+
+Select **Create new** from the labels dropdown list, provide a name, pick a
+color and hit **Create**.
+
+![Create new label on the fly](img/labels_new_label_on_the_fly_create.png)
+![New label on the fly](img/labels_new_label_on_the_fly.png)
+
+## Assigning labels to issues and merge requests
+
+There are generally two ways to assign a label to an issue or merge request.
+
+You can assign a label when you first create or edit an issue or merge request.
+
+![Assign label in new issue](img/labels_assign_label_in_new_issue.png)
+
+---
+
+The second way is by using the right sidebar when inside an issue or merge
+request. Expand it and hit **Edit** in the labels area. Start typing the name
+of the label you are looking for to narrow down the list, and select it. You
+can add more than one labels at once. When done, click outside the sidebar area
+for the changes to take effect.
+
+![Assign label in sidebar](img/labels_assign_label_sidebar.png)
+![Save labels in sidebar](img/labels_assign_label_sidebar_saved.png)
+
+---
+
+To remove labels, expand the left sidebar and unmark them from the labels list.
+Simple as that.
+
+## Use labels to filter issues
+
+Once you start adding labels to your issues, you'll see the benefit of it.
+Labels can have several uses, one of them being the quick filtering of issues
+or merge requests.
+
+Pick an existing label from the dropdown _Label_ menu or click on an existing
+label from the issue tracker. In the latter case, you also get to see the
+label description like shown below.
+
+![Filter labels](img/labels_filter.png)
+
+---
+
+And if you added a description to your label, you can see it by hovering your
+mouse over the label in the issue tracker or wherever else the label is
+rendered.
+
+![Label tooltips](img/labels_description_tooltip.png)
+
diff --git a/doc/user/project/settings/img/import_export_download_export.png b/doc/user/project/settings/img/import_export_download_export.png
new file mode 100644
index 00000000000..a2f7f0085c1
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_download_export.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_export_button.png b/doc/user/project/settings/img/import_export_export_button.png
new file mode 100644
index 00000000000..1f7bdd21b0d
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_export_button.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_mail_link.png b/doc/user/project/settings/img/import_export_mail_link.png
new file mode 100644
index 00000000000..c123f83eb8e
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_mail_link.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_new_project.png b/doc/user/project/settings/img/import_export_new_project.png
new file mode 100644
index 00000000000..b3a7f201018
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_new_project.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_select_file.png b/doc/user/project/settings/img/import_export_select_file.png
new file mode 100644
index 00000000000..f31832af3e1
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_select_file.png
Binary files differ
diff --git a/doc/user/project/settings/img/settings_edit_button.png b/doc/user/project/settings/img/settings_edit_button.png
new file mode 100644
index 00000000000..3c0cee536de
--- /dev/null
+++ b/doc/user/project/settings/img/settings_edit_button.png
Binary files differ
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
new file mode 100644
index 00000000000..38e9786123d
--- /dev/null
+++ b/doc/user/project/settings/import_export.md
@@ -0,0 +1,73 @@
+# Project import/export
+
+>**Notes:**
+ - This feature was [introduced][ce-3050] in GitLab 8.9
+ - Importing will not be possible if the import instance version is lower
+ than that of the exporter.
+ - For existing installations, the project import option has to be enabled in
+ application settings (`/admin/application_settings`) under 'Import sources'.
+ Ask your administrator if you don't see the **GitLab export** button when
+ creating a new project.
+ - You can find some useful raketasks if you are an administrator in the
+ [import_export](../../../administration/raketasks/project_import_export.md)
+ raketask.
+ - The exports are stored in a temporary [shared directory][tmp] and are deleted
+ every 24 hours by a specific worker.
+
+Existing projects running on any GitLab instance or GitLab.com can be exported
+with all their related data and be moved into a new GitLab instance.
+
+## Exported contents
+
+The following items will be exported:
+
+- Project and wiki repositories
+- Project uploads
+- Project configuration including web hooks and services
+- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets,
+ and other project entities
+
+The following items will NOT be exported:
+
+- Build traces and artifacts
+- LFS objects
+
+## Exporting a project and its data
+
+1. Go to the project settings page by clicking on **Edit Project**:
+
+ ![Project settings button](img/settings_edit_button.png)
+
+1. Scroll down to find the **Export project** button:
+
+ ![Export button](img/import_export_export_button.png)
+
+1. Once the export is generated, you should receive an e-mail with a link to
+ download the file:
+
+ ![Email download link](img/import_export_mail_link.png)
+
+1. Alternatively, you can come back to the project settings and download the
+ file from there, or generate a new export. Once the file available, the page
+ should show the **Download export** button:
+
+ ![Download export](img/import_export_download_export.png)
+
+## Importing the project
+
+1. The new GitLab project import feature is at the far right of the import
+ options when creating a New Project. Make sure you are in the right namespace
+ and you have entered a project name. Click on **GitLab export**:
+
+ ![New project](img/import_export_new_project.png)
+
+1. You can see where the project will be imported to. You can now select file
+ exported previously:
+
+ ![Select file](img/import_export_select_file.png)
+
+1. Click on **Import project** to begin importing. Your newly imported project
+ page will appear soon.
+
+[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[tmp]: ../../../development/shared_files.md
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 9efe41308dc..ddb2f7281b1 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -7,7 +7,7 @@
- [Groups](groups.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
-- [Labels](labels.md)
+- [Labels](../user/project/labels.md)
- [Notification emails](notifications.md)
- [Project Features](project_features.md)
- [Project forking workflow](forking_workflow.md)
diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md
index fffa0aba57f..4b551130255 100644
--- a/doc/workflow/add-user/add-user.md
+++ b/doc/workflow/add-user/add-user.md
@@ -8,7 +8,7 @@ You should have `master` or `owner` permissions to add or import a new user
to your project.
The first step to add or import a user, go to your project and click on
-**Members** on the left side of your screen.
+**Members** in the drop-down menu on the right side of your screen.
![Members](img/add_user_members_menu.png)
@@ -87,3 +87,25 @@ invitation, change their access level or even delete them.
Once the user accepts the invitation, they will be prompted to create a new
GitLab account using the same e-mail address the invitation was sent to.
+
+## Request access to a project
+
+As a user, you can request to be a member of a project. Go to the project you'd
+like to be a member of, and click the **Request Access** button on the right
+side of your screen.
+
+![Request access button](img/request_access_button.png)
+
+---
+
+Project owners & masters will be notified of your request and will be able to approve or
+decline it on the members page.
+
+![Manage access requests](img/access_requests_management.png)
+
+---
+
+If you change your mind before your request is approved, just click the
+**Withdraw Access Request** button.
+
+![Withdraw access request button](img/withdraw_access_request_button.png)
diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/workflow/add-user/img/access_requests_management.png
new file mode 100644
index 00000000000..e9641cb4f85
--- /dev/null
+++ b/doc/workflow/add-user/img/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png
index 910affc9659..18aabf93d50 100644
--- a/doc/workflow/add-user/img/add_user_email_accept.png
+++ b/doc/workflow/add-user/img/add_user_email_accept.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png
index 5f02ce89b3e..385d64330c0 100644
--- a/doc/workflow/add-user/img/add_user_email_ready.png
+++ b/doc/workflow/add-user/img/add_user_email_ready.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png
index 140979fbe13..84741edbca4 100644
--- a/doc/workflow/add-user/img/add_user_email_search.png
+++ b/doc/workflow/add-user/img/add_user_email_search.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png
index 8ef9156c8d5..7e580384e54 100644
--- a/doc/workflow/add-user/img/add_user_give_permissions.png
+++ b/doc/workflow/add-user/img/add_user_give_permissions.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
index 5770d5cf0c4..8dbd73a5bc8 100644
--- a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
+++ b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png
index dea4b3f40ad..abac1f59c02 100644
--- a/doc/workflow/add-user/img/add_user_imported_members.png
+++ b/doc/workflow/add-user/img/add_user_imported_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png
index 7daa6ca7d9e..e17d88c6f5f 100644
--- a/doc/workflow/add-user/img/add_user_list_members.png
+++ b/doc/workflow/add-user/img/add_user_list_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/workflow/add-user/img/add_user_members_menu.png
index f1797b95f67..ec5d39f402d 100644
--- a/doc/workflow/add-user/img/add_user_members_menu.png
+++ b/doc/workflow/add-user/img/add_user_members_menu.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png
index 5ac10ce80d4..eaa062376f4 100644
--- a/doc/workflow/add-user/img/add_user_search_people.png
+++ b/doc/workflow/add-user/img/add_user_search_people.png
Binary files differ
diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/workflow/add-user/img/request_access_button.png
new file mode 100644
index 00000000000..984d640b0f0
--- /dev/null
+++ b/doc/workflow/add-user/img/request_access_button.png
Binary files differ
diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/workflow/add-user/img/withdraw_access_request_button.png
new file mode 100644
index 00000000000..ff54a0e4384
--- /dev/null
+++ b/doc/workflow/add-user/img/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index 70b35c58be6..e6f8b792707 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,28 +1,26 @@
-# Award emojis
+# Award emoji
>**Note:**
This feature was [introduced][1825] in GitLab 8.2.
When you're collaborating online, you get fewer opportunities for high-fives
-and thumbs-ups. In order to make virtual celebrations easier, you can now vote
-on issues and merge requests using emoji!
+and thumbs-ups. Emoji can be awarded to issues and merge requests, making
+virtual celebrations easier.
![Award emoji](img/award_emoji_select.png)
-This makes it much easier to give and receive feedback, without a long comment
-thread. Any comment that contains only the thumbs up or down emojis is
-converted to a vote and depicted in the emoji area.
-
-You can then use that functionality to sort issues and merge requests based on
-popularity.
+Award emoji make it much easier to give and receive feedback without a long
+comment thread. Comments that are only emoji will automatically become
+award emoji.
## Sort issues and merge requests on vote count
>**Note:**
This feature was [introduced][2871] in GitLab 8.5.
-You can quickly sort the issues or merge requests by the number of votes they
-have received. The sort option can be found in the right dropdown menu.
+You can quickly sort issues and merge requests by the number of votes they
+have received. The sort options can be found in the dropdown menu as "Most
+popular" and "Least popular".
![Votes sort options](img/award_emoji_votes_sort_options.png)
@@ -40,9 +38,28 @@ Sort by least popular issues/merge requests.
---
-The number of upvotes and downvotes is not summed up. That means that an issue
-with 18 upvotes and 5 downvotes is considered more popular than an issue with
-17 upvotes and no downvotes.
+The total number of votes is not summed up. An issue with 18 upvotes and 5
+downvotes is considered more popular than an issue with 17 upvotes and no
+downvotes.
+
+## Award emoji for comments
+
+>**Note:**
+This feature was [introduced][4291] in GitLab 8.9.
+
+Award emoji can also be applied to individual comments when you want to
+celebrate an accomplishment or agree with an opinion.
+
+To add an award emoji, click the smile in the top right of the comment and pick
+an emoji from the dropdown.
+
+![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
+
+![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
+
+If you want to remove an award emoji, just click the emoji again and the vote
+will be removed.
[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
+[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png
index fb26ee04393..3408ed95841 100644
--- a/doc/workflow/award_emoji.png
+++ b/doc/workflow/award_emoji.png
Binary files differ
diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md
index 34ada1774d8..1a316e80976 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -51,6 +51,28 @@ If necessary, you can increase the access level of an individual user for a spec
![Barry effectively has 'Master' access to GitLab CI now](groups/override_access_level.png)
+## Request access to a group
+
+As a user, you can request to be a member of a group. Go to the group you'd
+like to be a member of, and click the **Request Access** button on the right
+side of your screen.
+
+![Request access button](groups/request_access_button.png)
+
+---
+
+Group owners & masters will be notified of your request and will be able to approve or
+decline it on the members page.
+
+![Manage access requests](groups/access_requests_management.png)
+
+---
+
+If you change your mind before your request is approved, just click the
+**Withdraw Access Request** button.
+
+![Withdraw access request button](groups/withdraw_access_request_button.png)
+
## Managing group memberships via LDAP
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
diff --git a/doc/workflow/groups/access_requests_management.png b/doc/workflow/groups/access_requests_management.png
new file mode 100644
index 00000000000..ffede8e9bd6
--- /dev/null
+++ b/doc/workflow/groups/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/groups/request_access_button.png b/doc/workflow/groups/request_access_button.png
new file mode 100644
index 00000000000..ff0ac8747a7
--- /dev/null
+++ b/doc/workflow/groups/request_access_button.png
Binary files differ
diff --git a/doc/workflow/groups/withdraw_access_request_button.png b/doc/workflow/groups/withdraw_access_request_button.png
new file mode 100644
index 00000000000..99d7a326ed8
--- /dev/null
+++ b/doc/workflow/groups/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/workflow/img/award_emoji_comment_awarded.png
new file mode 100644
index 00000000000..67697831869
--- /dev/null
+++ b/doc/workflow/img/award_emoji_comment_awarded.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/workflow/img/award_emoji_comment_picker.png
new file mode 100644
index 00000000000..d9c3faecdca
--- /dev/null
+++ b/doc/workflow/img/award_emoji_comment_picker.png
Binary files differ
diff --git a/doc/workflow/labels.md b/doc/workflow/labels.md
index 6e4840ca5ae..5c09891dfdd 100644
--- a/doc/workflow/labels.md
+++ b/doc/workflow/labels.md
@@ -1,18 +1,3 @@
# Labels
-In GitLab, you can easily tag issues and Merge Requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`.
-
-Here you can create a new label.
-
-![new label](labels/label1.png)
-
-You can choose to set a color.
-
-![label color](labels/label2.png)
-
-If you want to change an existing label, press edit next to the listed label.
-You will be presented with the same form as when creating a new label.
-
-![edit label](labels/label3.png)
-
-You can add labels to Merge Requests when you create or edit them.
+This document was moved to [user/project/labels.md](../user/project/labels.md).
diff --git a/doc/workflow/labels/label1.png b/doc/workflow/labels/label1.png
deleted file mode 100644
index cac661a34c8..00000000000
--- a/doc/workflow/labels/label1.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/labels/label2.png b/doc/workflow/labels/label2.png
deleted file mode 100644
index 44d9fef86d4..00000000000
--- a/doc/workflow/labels/label2.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/labels/label3.png b/doc/workflow/labels/label3.png
deleted file mode 100644
index e2fce11b7a4..00000000000
--- a/doc/workflow/labels/label3.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 98f44a5b07b..fe4485e148a 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -20,7 +20,7 @@ Each of these settings have levels of notification:
* Participating - receive notifications from related resources
* Watch - receive notifications from projects or groups user is a member of
* Global - notifications as set at the global settings
-* Custom - Notification is set based on events selected by the user.(Available only in project level)
+* Custom - user will receive notifications when mentioned, is participant and custom selected events.
#### Global Settings
diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png
index 1628e2d568c..7c6857aad1a 100644
--- a/doc/workflow/notifications/settings.png
+++ b/doc/workflow/notifications/settings.png
Binary files differ
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png
index beb6c53ec77..16be0413b64 100644
--- a/doc/workflow/shortcuts.png
+++ b/doc/workflow/shortcuts.png
Binary files differ
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/admin/groups.feature b/features/admin/groups.feature
index ab7de7ac315..657e847cf4a 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -27,13 +27,6 @@ Feature: Admin Groups
Then I should see project shared with group
@javascript
- Scenario: Remove user from group
- Given we have user "John Doe" in group
- When I visit admin group page
- And I remove user "John Doe" from group
- Then I should not see "John Doe" in team list
-
- @javascript
Scenario: Invite user to a group by e-mail
When I visit admin group page
When I select user "johndoe@gitlab.com" from user list as "Reporter"
diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature
index e3c01db2ebb..3ae2c679dc1 100644
--- a/features/dashboard/group.feature
+++ b/features/dashboard/group.feature
@@ -5,53 +5,9 @@ Feature: Dashboard Group
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
- # Leave groups
-
- @javascript
- Scenario: Owner should be able to leave from group if he is not the last owner
- Given "Mary Jane" is owner of group "Owned"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Owned"
- And I visit dashboard groups page
- Then I should not see group "Owned" in group list
- Then I should see group "Guest" in group list
-
- @javascript
- Scenario: Owner should not be able to leave from group if he is the last owner
- Given "Mary Jane" is guest of group "Owned"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Owned"
- Then I should see the "Can not leave message"
-
- @javascript
- Scenario: Guest should be able to leave from group
- Given "Mary Jane" is guest of group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should not see group "Guest" in group list
-
- @javascript
- Scenario: Guest should be able to leave from group even if he is the only user in the group
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should not see group "Guest" in group list
-
Scenario: Create a group from dasboard
And I visit dashboard groups page
And I click new group link
And submit form with new group "Samurai" info
Then I should be redirected to group "Samurai" page
And I should see newly created group "Samurai"
-
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/dashboard/todos.feature b/features/dashboard/todos.feature
index 8677b450813..42f5d6d2af7 100644
--- a/features/dashboard/todos.feature
+++ b/features/dashboard/todos.feature
@@ -14,7 +14,12 @@ Feature: Dashboard Todos
Scenario: I mark todos as done
Then I should see todos assigned to me
And I mark the todo as done
- And I click on the "Done" tab
+ Then I should see the todo marked as done
+
+ @javascript
+ Scenario: I mark all todos as done
+ Then I should see todos assigned to me
+ And I mark all todos as done
Then I should see all todos marked as done
@javascript
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index c4f987a7923..57dda9c2234 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -10,9 +10,9 @@ Feature: Project Active Tab
Then the active main tab should be Home
And no other main tabs should be active
- Scenario: On Project Code
+ Scenario: On Project Repository
Given I visit my project's files page
- Then the active main tab should be Code
+ Then the active main tab should be Repository
And no other main tabs should be active
Scenario: On Project Issues
@@ -59,46 +59,46 @@ Feature: Project Active Tab
And no other sub navs should be active
And the active main tab should be Settings
- # Sub Tabs: Code
+ # Sub Tabs: Repository
- Scenario: On Project Code/Files
+ Scenario: On Project Repository/Files
Given I visit my project's files page
Then the active sub tab should be Files
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Commits
+ Scenario: On Project Repository/Commits
Given I visit my project's commits page
Then the active sub tab should be Commits
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Network
+ Scenario: On Project Repository/Network
Given I visit my project's network page
Then the active sub tab should be Network
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Compare
+ Scenario: On Project Repository/Compare
Given I visit my project's commits page
And I click the "Compare" tab
Then the active sub tab should be Compare
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Branches
+ Scenario: On Project Repository/Branches
Given I visit my project's commits page
And I click the "Branches" tab
Then the active sub tab should be Branches
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Tags
+ Scenario: On Project Repository/Tags
Given I visit my project's commits page
And I click the "Tags" tab
Then the active sub tab should be Tags
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
Scenario: On Project Issues/Browse
Given I visit my project's issues page
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
index c73d0b32337..f71f69ef060 100644
--- a/features/project/shortcuts.feature
+++ b/features/project/shortcuts.feature
@@ -8,21 +8,21 @@ Feature: Project Shortcuts
@javascript
Scenario: Navigate to files tab
Given I press "g" and "f"
- Then the active main tab should be Code
+ Then the active main tab should be Repository
Then the active sub tab should be Files
@javascript
Scenario: Navigate to commits tab
Given I visit my project's files page
Given I press "g" and "c"
- Then the active main tab should be Code
+ Then the active main tab should be Repository
Then the active sub tab should be Commits
@javascript
Scenario: Navigate to network tab
Given I press "g" and "n"
Then the active sub tab should be Network
- And the active main tab should be Code
+ And the active main tab should be Repository
@javascript
Scenario: Navigate to graphs tab
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/admin/groups.rb b/features/steps/admin/groups.rb
index e1f1db2872f..8613dc537cc 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -62,7 +62,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do
page.within ".group-users-list" do
- expect(page).to have_content "johndoe@gitlab.com (invited)"
+ expect(page).to have_content "johndoe@gitlab.com – Invited by"
expect(page).to have_content "Reporter"
end
end
@@ -92,12 +92,6 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
current_group.add_reporter(user_john)
end
- step 'I remove user "John Doe" from group' do
- page.within "#user_#{user_john.id}" do
- click_link 'Remove user from group'
- end
- end
-
step 'I should not see "John Doe" in team list' do
page.within ".group-users-list" do
expect(page).not_to have_content "John Doe"
diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb
index 0c6a0ae3725..cf679fea530 100644
--- a/features/steps/dashboard/group.rb
+++ b/features/steps/dashboard/group.rb
@@ -4,44 +4,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
include SharedPaths
include SharedUser
- # Leave
-
- step 'I click on the "Leave" button for group "Owned"' do
- find(:css, 'li', text: "Owner").find(:css, 'i.fa.fa-sign-out').click
- # poltergeist always confirms popups.
- end
-
- step 'I click on the "Leave" button for group "Guest"' do
- find(:css, 'li', text: "Guest").find(:css, 'i.fa.fa-sign-out').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Leave" button for group "Owned"' do
- expect(find(:css, 'li', text: "Owner")).not_to have_selector(:css, 'i.fa.fa-sign-out')
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Leave" button for groupr "Guest"' do
- expect(find(:css, 'li', text: "Guest")).not_to have_selector(:css, 'i.fa.fa-sign-out')
- # poltergeist always confirms popups.
- end
-
- step 'I should see group "Owned" in group list' do
- expect(page).to have_content("Owned")
- end
-
- step 'I should not see group "Owned" in group list' do
- expect(page).not_to have_content("Owned")
- end
-
- step 'I should see group "Guest" in group list' do
- expect(page).to have_content("Guest")
- end
-
- step 'I should not see group "Guest" in group list' do
- expect(page).not_to have_content("Guest")
- end
-
step 'I click new group link' do
click_link "New Group"
end
@@ -60,8 +22,4 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
expect(page).to have_content "Samurai"
expect(page).to have_content "Tokugawa Shogunate"
end
-
- step 'I should see the "Can not leave message"' do
- expect(page).to have_content "You can not leave Owned group because you're the last owner"
- 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/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 19fedfbfcdf..60152d3da55 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -26,14 +26,15 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should see todos assigned to me' do
+ page.within('.todos-pending-count') { expect(page).to have_content '4' }
expect(page).to have_content 'To do 4'
expect(page).to have_content 'Done 0'
expect(page).to have_link project.name_with_namespace
should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title)
- should_see_todo(2, "John Doe mentioned you on issue ##{issue.iid}", "#{current_user.to_reference} Wdyt?")
- should_see_todo(3, "John Doe assigned you issue ##{issue.iid}", issue.title)
- should_see_todo(4, "Mary Jane mentioned you on issue ##{issue.iid}", issue.title)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?")
+ should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title)
end
step 'I mark the todo as done' do
@@ -41,18 +42,40 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done'
end
+ page.within('.todos-pending-count') { expect(page).to have_content '3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
end
- step 'I click on the "Done" tab' do
+ step 'I mark all todos as done' do
+ click_link 'Mark all as done'
+
+ page.within('.todos-pending-count') { expect(page).to have_content '0' }
+ expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Done 4'
+ expect(page).not_to have_link project.name_with_namespace
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
+ should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}"
+ should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
+ end
+
+ step 'I should see the todo marked as done' do
click_link 'Done 1'
+
+ expect(page).to have_link project.name_with_namespace
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
end
step 'I should see all todos marked as done' do
+ click_link 'Done 4'
+
expect(page).to have_link project.name_with_namespace
should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?", false)
+ should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title, false)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title, false)
end
step 'I filter by "Enterprise"' do
@@ -76,7 +99,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should not see todos related to "Mary Jane" in the list' do
- should_not_see_todo "Mary Jane mentioned you on issue ##{issue.iid}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
end
step 'I should not see todos related to "Merge Requests" in the list' do
@@ -85,7 +108,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
step 'I should not see todos related to "Assignments" in the list' do
should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
- should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
+ should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
end
step 'I click on the todo' do
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index 0706df3aec5..dfa2fa75def 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -53,7 +53,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
page.within '.content-list' do
expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('invited')
+ expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
@@ -116,11 +116,9 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- find(".js-toggle-button").click
- page.within "#edit_group_member_#{member.id}" do
- select 'Developer', from: 'group_member_access_level'
- click_on 'Save'
- end
+ click_button "Edit access level"
+ select 'Developer', from: 'group_member_access_level'
+ click_on 'Save'
end
end
@@ -128,9 +126,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- page.within '.member-access-level' do
- expect(page).to have_content "Developer"
- end
+ expect(page).to have_content "Developer"
end
end
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
index a96f35ada51..7e339443b75 100644
--- a/features/steps/profile/notifications.rb
+++ b/features/steps/profile/notifications.rb
@@ -11,12 +11,10 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
end
step 'I select Mention setting from dropdown' do
- select 'mention', from: 'notification_setting_level'
+ first(:link, "On mention").trigger('click')
end
step 'I should see Notification saved message' do
- page.within '.flash-container' do
- expect(page).to have_content 'Notification settings saved'
- end
+ expect(page).to have_content 'On mention'
end
end
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 9b59b682676..019b3124a86 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -20,11 +20,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
step 'page should select "master" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "master"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
end
step 'page should select "v1.0.0" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "v1.0.0"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0"
end
step 'page should have "master" on graph' do
@@ -40,11 +40,19 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
When 'I switch ref to "feature"' do
- select 'feature', from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'feature'
+ end
end
When 'I switch ref to "v1.0.0"' do
- select 'v1.0.0', from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'v1.0.0'
+ end
end
When 'click "Show only selected branch" checkbox' do
@@ -68,11 +76,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
step 'page should select "feature" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "feature"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "feature"
end
step 'page should select "v1.0.0" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "v1.0.0"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0"
end
step 'page should have "feature" on graph' do
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 98b57e5cbfb..76fefee9254 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -134,8 +134,8 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see Notification saved message' do
- page.within '.flash-container' do
- expect(page).to have_content 'Notification settings saved'
+ page.within '#notifications-button' do
+ expect(page).to have_content 'On mention'
end
end
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
index 47de4b91df1..90771847909 100644
--- a/features/steps/project/project_find_file.rb
+++ b/features/steps/project/project_find_file.rb
@@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
end
step 'I should see "find file" page' do
- ensure_active_main_tab('Code')
+ ensure_active_main_tab('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
step 'I fill in Find by path with "git"' do
- ensure_active_main_tab('Code')
+ ensure_active_main_tab('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 2c0498de3b9..0fe046dcbf6 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
@@ -290,15 +290,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step "I switch ref to 'test'" do
- select "'test'", from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'test'
+ end
end
step "I switch ref to fix" do
- select "fix", from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'fix'
+ end
end
step "I see the ref 'test' has been selected" do
- expect(page).to have_selector '.select2-chosen', text: "'test'"
+ expect(page).to have_selector '.dropdown-toggle-text', text: "'test'"
end
step "I visit the 'test' tree" do
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index c6ced747370..f32576d2cb1 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -26,8 +26,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "Mike" in team list as "Reporter"' do
- page.within ".access-reporter" do
+ user = User.find_by(name: 'Mike')
+ project_member = project.project_members.find_by(user_id: user.id)
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Mike')
+ expect(page).to have_content('Reporter')
end
end
@@ -40,16 +43,20 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
- page.within ".access-reporter" do
+ project_member = project.project_members.find_by(invite_email: 'sjobs@apple.com')
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('invited')
+ expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
step 'I should see "Dmitriy" in team list as "Developer"' do
- page.within ".access-developer" do
+ user = User.find_by(name: 'Dmitriy')
+ project_member = project.project_members.find_by(user_id: user.id)
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
+ expect(page).to have_content('Developer')
end
end
@@ -65,15 +72,14 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I should see "Dmitriy" in team list as "Reporter"' do
- page.within ".access-reporter" do
+ user = User.find_by(name: 'Dmitriy')
+ project_member = project.project_members.find_by(user_id: user.id)
+ page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
+ expect(page).to have_content('Reporter')
end
end
- step 'I click link "Remove from team"' do
- click_link "Remove from team"
- end
-
step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy")
expect(page).not_to have_content(user.name)
@@ -120,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- click_link('Remove user from team')
+ click_link('Remove user from project')
end
end
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index bfee8793301..d6024212601 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -8,8 +8,8 @@ module SharedProjectTab
ensure_active_main_tab('Project')
end
- step 'the active main tab should be Code' do
- ensure_active_main_tab('Code')
+ step 'the active main tab should be Repository' do
+ ensure_active_main_tab('Repository')
end
step 'the active main tab should be Graphs' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6cd909f6115..f8f680a6311 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -26,38 +26,40 @@ module API
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
helpers ::API::Helpers
- mount ::API::Groups
+ mount ::API::AwardEmoji
+ mount ::API::Branches
+ mount ::API::Builds
+ mount ::API::CommitStatuses
+ mount ::API::Commits
+ mount ::API::DeployKeys
+ mount ::API::Files
mount ::API::GroupMembers
- mount ::API::Users
- mount ::API::Projects
- mount ::API::Repositories
+ mount ::API::Groups
+ mount ::API::Internal
mount ::API::Issues
- mount ::API::Milestones
- mount ::API::Session
+ mount ::API::Keys
+ mount ::API::Labels
+ mount ::API::Licenses
mount ::API::MergeRequests
+ mount ::API::Milestones
+ mount ::API::Namespaces
mount ::API::Notes
- mount ::API::Internal
- mount ::API::SystemHooks
- mount ::API::ProjectSnippets
- mount ::API::ProjectMembers
- mount ::API::DeployKeys
mount ::API::ProjectHooks
+ mount ::API::ProjectMembers
+ mount ::API::ProjectSnippets
+ mount ::API::Projects
+ mount ::API::Repositories
+ mount ::API::Runners
mount ::API::Services
- mount ::API::Files
- mount ::API::Commits
- mount ::API::CommitStatuses
- mount ::API::Namespaces
- mount ::API::Branches
- mount ::API::Labels
+ mount ::API::Session
mount ::API::Settings
- mount ::API::Keys
+ mount ::API::SidekiqMetrics
+ mount ::API::Subscriptions
+ mount ::API::SystemHooks
mount ::API::Tags
+ mount ::API::Templates
mount ::API::Triggers
- mount ::API::Builds
+ mount ::API::Users
mount ::API::Variables
- mount ::API::Runners
- mount ::API::Licenses
- mount ::API::Subscriptions
- mount ::API::Gitignores
end
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
new file mode 100644
index 00000000000..985590312e3
--- /dev/null
+++ b/lib/api/award_emoji.rb
@@ -0,0 +1,116 @@
+module API
+ class AwardEmoji < Grape::API
+ before { authenticate! }
+ AWARDABLES = [Issue, MergeRequest]
+
+ resource :projects do
+ AWARDABLES.each do |awardable_type|
+ awardable_string = awardable_type.to_s.underscore.pluralize
+ awardable_id_string = "#{awardable_type.to_s.underscore}_id"
+
+ [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
+ ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
+ ].each do |endpoint|
+
+ # Get a list of project +awardable+ award emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or MR
+ # Example Request:
+ # GET /projects/:id/issues/:awardable_id/award_emoji
+ get endpoint do
+ if can_read_awardable?
+ awards = paginate(awardable.award_emoji)
+ present awards, with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji")
+ end
+ end
+
+ # Get a specific award emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or MR
+ # award_id (required) - The ID of the award
+ # Example Request:
+ # GET /projects/:id/issues/:awardable_id/award_emoji/:award_id
+ get "#{endpoint}/:award_id" do
+ if can_read_awardable?
+ present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji")
+ end
+ end
+
+ # Award a new Emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or mr
+ # name (required) - The name of a award_emoji (without colons)
+ # Example Request:
+ # POST /projects/:id/issues/:awardable_id/award_emoji
+ post endpoint do
+ required_attributes! [:name]
+
+ not_found!('Award Emoji') unless can_read_awardable?
+
+ award = awardable.award_emoji.new(name: params[:name], user: current_user)
+
+ if award.save
+ present award, with: Entities::AwardEmoji
+ else
+ not_found!("Award Emoji #{award.errors.messages}")
+ end
+ end
+
+ # Delete a +awardables+ award emoji
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # awardable_id (required) - The ID of an issue or MR
+ # award_emoji_id (required) - The ID of an award emoji
+ # Example Request:
+ # DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+ delete "#{endpoint}/:award_id" do
+ award = awardable.award_emoji.find(params[:award_id])
+
+ unauthorized! unless award.user == current_user || current_user.admin?
+
+ award.destroy
+ present award, with: Entities::AwardEmoji
+ end
+ end
+ end
+ end
+
+ helpers do
+ def can_read_awardable?
+ ability = "read_#{awardable.class.to_s.underscore}".to_sym
+
+ can?(current_user, ability, awardable)
+ end
+
+ def awardable
+ @awardable ||=
+ begin
+ if params.include?(:note_id)
+ noteable.notes.find(params[:note_id])
+ else
+ noteable
+ end
+ end
+ end
+
+ def noteable
+ if params.include?(:issue_id)
+ user_project.issues.find(params[:issue_id])
+ else
+ user_project.merge_requests.find(params[:merge_request_id])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index 0ff8fa74a84..979328efe0e 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -142,7 +142,7 @@ module API
return not_found!(build) unless build
return forbidden!('Build is not retryable') unless build.retryable?
- build = Ci::Build.retry(build)
+ build = Ci::Build.retry(build, current_user)
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
@@ -166,6 +166,26 @@ module API
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
end
+
+ # Keep the artifacts to prevent them from being deleted
+ #
+ # Parameters:
+ # id (required) - the id of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # POST /projects/:id/builds/:build_id/artifacts/keep
+ post ':id/builds/:build_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build && build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
end
helpers do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 76f40df96ae..5a23a18fe9c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -88,10 +88,7 @@ module API
class Group < Grape::Entity
expose :id, :name, :path, :description, :visibility_level
expose :avatar_url
-
- expose :web_url do |group, options|
- Gitlab::Routing.url_helpers.group_url(group)
- end
+ expose :web_url
end
class GroupDetail < Group
@@ -228,6 +225,14 @@ module API
expose(:downvote?) { |note| false }
end
+ class AwardEmoji < Grape::Entity
+ expose :id
+ expose :name
+ expose :user, using: Entities::UserBasic
+ expose :created_at, :updated_at
+ expose :awardable_id, :awardable_type
+ end
+
class MRNote < Grape::Entity
expose :note
expose :author, using: Entities::UserBasic
@@ -275,8 +280,7 @@ module API
expose :access_level
expose :notification_level do |member, options|
if member.notification_setting
- setting = member.notification_setting
- { level: NotificationSetting.levels[setting.level], events: setting.events }
+ NotificationSetting.levels[member.notification_setting.level]
end
end
end
@@ -419,6 +423,7 @@ module API
class RunnerDetails < Runner
expose :tag_list
expose :run_untagged
+ expose :locked
expose :version, :revision, :platform, :architecture
expose :contacted_at
expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
@@ -440,11 +445,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: User
expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
- expose :commit, with: RepoCommit do |repo_obj, _options|
- if repo_obj.respond_to?(:commit)
- repo_obj.commit.commit_data
- end
- end
+ expose :commit, with: RepoCommit
expose :runner, with: Runner
end
@@ -468,11 +469,11 @@ module API
expose :content
end
- class GitignoresList < Grape::Entity
+ class TemplatesList < Grape::Entity
expose :name
end
- class Gitignore < Grape::Entity
+ class Template < Grape::Entity
expose :name, :content
end
end
diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb
deleted file mode 100644
index 270c9501dd2..00000000000
--- a/lib/api/gitignores.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module API
- class Gitignores < Grape::API
-
- # Get the list of the available gitignore templates
- #
- # Example Request:
- # GET /gitignores
- get 'gitignores' do
- present Gitlab::Gitignore.all, with: Entities::GitignoresList
- end
-
- # Get the text for a specific gitignore
- #
- # Parameters:
- # name (required) - The name of a license
- #
- # Example Request:
- # GET /gitignores/Elixir
- #
- get 'gitignores/:name' do
- required_attributes! [:name]
-
- gitignore = Gitlab::Gitignore.find(params[:name])
- not_found!('.gitignore') unless gitignore
-
- present gitignore, with: Entities::Gitignore
- end
- 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/internal.rb b/lib/api/internal.rb
index 3ac7b50c4ce..1d361569d59 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -23,8 +23,6 @@ module API
end
post "/allowed" do
- Gitlab::Metrics.action = 'Grape#/internal/allowed'
-
status 200
actor =
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index d4fcfd3d4d3..8bfa998dc53 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -144,7 +144,7 @@ module API
helpers do
def noteable_read_ability_name(noteable)
- "read_#{noteable.class.to_s.underscore.downcase}".to_sym
+ "read_#{noteable.class.to_s.underscore}".to_sym
end
end
end
diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb
index 4aefdf319c6..b703da0557a 100644
--- a/lib/api/project_members.rb
+++ b/lib/api/project_members.rb
@@ -46,7 +46,7 @@ module API
required_attributes! [:user_id, :access_level]
# either the user is already a team member or a new one
- project_member = user_project.project_member_by_id(params[:user_id])
+ project_member = user_project.project_member(params[:user_id])
if project_member.nil?
project_member = user_project.project_members.new(
user_id: params[:user_id],
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 4faba9dc87b..ecc8f2fc5a2 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -49,7 +49,7 @@ module API
runner = get_runner(params[:id])
authenticate_update_runner!(runner)
- attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged]
+ attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
if runner.update(attrs)
present runner, with: Entities::RunnerDetails, current_user: current_user
else
@@ -96,9 +96,14 @@ module API
runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner)
- Ci::RunnerProject.create(runner: runner, project: user_project)
- present runner, with: Entities::Runner
+ runner_project = runner.assign_to(user_project)
+
+ if runner_project.persisted?
+ present runner, with: Entities::Runner
+ else
+ conflict!("Runner was already enabled for this project")
+ end
end
# Disable project's runner
@@ -163,6 +168,7 @@ module API
def authenticate_enable_runner!(runner)
forbidden!("Runner is shared") if runner.is_shared?
+ forbidden!("Runner is locked") if runner.locked?
return if current_user.is_admin?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
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/api/templates.rb b/lib/api/templates.rb
new file mode 100644
index 00000000000..18408797756
--- /dev/null
+++ b/lib/api/templates.rb
@@ -0,0 +1,36 @@
+module API
+ class Templates < Grape::API
+ TEMPLATE_TYPES = {
+ gitignores: Gitlab::Template::Gitignore,
+ gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
+ }.freeze
+
+ TEMPLATE_TYPES.each do |template, klass|
+ # Get the list of the available template
+ #
+ # Example Request:
+ # GET /gitignores
+ # GET /gitlab_ci_ymls
+ get template.to_s do
+ present klass.all, with: Entities::TemplatesList
+ end
+
+ # Get the text for a specific template
+ #
+ # Parameters:
+ # name (required) - The name of a template
+ #
+ # Example Request:
+ # GET /gitignores/Elixir
+ # GET /gitlab_ci_ymls/Ruby
+ get "#{template}/:name" do
+ required_attributes! [:name]
+
+ new_template = klass.find(params[:name])
+ not_found!(template.to_s.singularize) unless new_template
+
+ present new_template, with: Entities::Template
+ end
+ end
+ end
+end
diff --git a/lib/banzai.rb b/lib/banzai.rb
index b467413a7dd..093382261ae 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -7,10 +7,6 @@ module Banzai
Renderer.render_result(text, context)
end
- def self.pre_process(text, context)
- Renderer.pre_process(text, context)
- end
-
def self.post_process(html, context)
Renderer.post_process(html, context)
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index db95d7c908b..81d66271136 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,56 @@ 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
+ symbol = $~[object_sym]
+
+ refs[project] << symbol if object_class.reference_valid?(symbol)
+ 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/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index ea21c7b041c..c78da404607 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -14,6 +14,8 @@ module Banzai
def call
return doc unless linkable_files?
+ @uri_types = {}
+
doc.search('a:not(.gfm)').each do |el|
process_link_attr el.attribute('href')
end
@@ -48,7 +50,7 @@ module Banzai
uri.path = [
relative_url_root,
context[:project].path_with_namespace,
- path_type(file_path),
+ uri_type(file_path),
ref || context[:project].default_branch, # if no ref exists, point to the default branch
file_path
].compact.join('/').squeeze('/').chomp('/')
@@ -87,7 +89,7 @@ module Banzai
return path unless request_path
parts = request_path.split('/')
- parts.pop if path_type(request_path) != 'tree'
+ parts.pop if uri_type(request_path) != :tree
while path.start_with?('../')
parts.pop
@@ -98,45 +100,20 @@ module Banzai
end
def file_exists?(path)
- return false if path.nil?
- repository.blob_at(current_sha, path).present? ||
- repository.tree(current_sha, path).entries.any?
- end
-
- # Get the type of the given path
- #
- # path - String path to check
- #
- # Examples:
- #
- # path_type('doc/README.md') # => 'blob'
- # path_type('doc/logo.png') # => 'raw'
- # path_type('doc/api') # => 'tree'
- #
- # Returns a String
- def path_type(path)
- unescaped_path = Addressable::URI.unescape(path)
-
- if tree?(unescaped_path)
- 'tree'
- elsif image?(unescaped_path)
- 'raw'
- else
- 'blob'
- end
+ path.present? && !!uri_type(path)
end
- def tree?(path)
- repository.tree(current_sha, path).entries.any?
- end
+ def uri_type(path)
+ @uri_types[path] ||= begin
+ unescaped_path = Addressable::URI.unescape(path)
- def image?(path)
- repository.blob_at(current_sha, path).try(:image?)
+ current_commit.uri_type(unescaped_path)
+ end
end
- def current_sha
- context[:commit].try(:id) ||
- ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha
+ def current_commit
+ @current_commit ||= context[:commit] ||
+ ref ? repository.commit(ref) : repository.head_commit
end
def relative_url_root
@@ -148,7 +125,7 @@ module Banzai
end
def repository
- context[:project].try(:repository)
+ @repository ||= context[:project].try(:repository)
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/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb
index 37a2779d453..1bb6d6bba87 100644
--- a/lib/banzai/filter/wiki_link_filter.rb
+++ b/lib/banzai/filter/wiki_link_filter.rb
@@ -29,7 +29,7 @@ module Banzai
return if html_attr.blank?
html_attr.value = apply_rewrite_rules(html_attr.value)
- rescue URI::Error
+ rescue URI::Error, Addressable::URI::InvalidURIError
# noop
end
diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb
index f2395867658..042fb2e6e14 100644
--- a/lib/banzai/pipeline/description_pipeline.rb
+++ b/lib/banzai/pipeline/description_pipeline.rb
@@ -1,23 +1,16 @@
module Banzai
module Pipeline
class DescriptionPipeline < FullPipeline
+ WHITELIST = Banzai::Filter::SanitizationFilter::LIMITED.deep_dup.merge(
+ elements: Banzai::Filter::SanitizationFilter::LIMITED[:elements] - %w(pre code img ol ul li)
+ )
+
def self.transform_context(context)
super(context).merge(
# SanitizationFilter
- whitelist: whitelist
+ whitelist: WHITELIST
)
end
-
- private
-
- def self.whitelist
- # Descriptions are more heavily sanitized, allowing only a few elements.
- # See http://git.io/vkuAN
- whitelist = Banzai::Filter::SanitizationFilter::LIMITED
- whitelist[:elements] -= %w(pre code img ol ul li)
-
- whitelist
- end
end
end
end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index 24076e3d9ec..f306079d833 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -25,7 +25,21 @@ module Banzai
def issues_for_nodes(nodes)
@issues_for_nodes ||= grouped_objects_for_nodes(
nodes,
- Issue.all.includes(:author, :assignee, :project),
+ Issue.all.includes(
+ :author,
+ :assignee,
+ {
+ # These associations are primarily used for checking permissions.
+ # Eager loading these ensures we don't end up running dozens of
+ # queries in this process.
+ project: [
+ { namespace: :owner },
+ { group: [:owners, :group_members] },
+ :invited_groups,
+ :project_members
+ ]
+ }
+ ),
self.class.data_attribute
)
end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index c14a9c4c722..6718acdef7e 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -30,13 +30,9 @@ module Banzai
end
def self.render_result(text, context = {})
- Pipeline[context[:pipeline]].call(text, context)
- end
+ text = Pipeline[:pre_process].to_html(text, context) if text
- def self.pre_process(text, context)
- pipeline = Pipeline[:pre_process]
-
- pipeline.to_html(text, context)
+ Pipeline[context[:pipeline]].call(text, context)
end
# Perform post-processing on an HTML String
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 607359769d1..9f270f7b387 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -114,6 +114,7 @@ module Ci
# id (required) - The ID of a build
# token (required) - The build authorization token
# file (required) - Artifacts file
+ # expire_in (optional) - Specify when artifacts should expire (ex. 7d)
# Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition
@@ -145,6 +146,7 @@ module Ci
build.artifacts_file = artifacts
build.artifacts_metadata = metadata
+ build.artifacts_expire_in = params['expire_in']
if build.save
present(build, with: Entities::BuildDetails)
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index a902ced35d7..3f5bdaba3f5 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -20,7 +20,7 @@ module Ci
expose :name, :token, :stage
expose :project_id
expose :project_name
- expose :artifacts_file, using: ArtifactFile, if: lambda { |build, opts| build.artifacts? }
+ expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? }
end
class BuildDetails < Build
@@ -29,6 +29,7 @@ module Ci
expose :before_sha
expose :allow_git_fetch
expose :token
+ expose :artifacts_expire_at, if: ->(build, _) { build.artifacts? }
expose :options do |model|
model.options
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index 0c41f22c7c5..bcc82969eb3 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -28,12 +28,9 @@ module Ci
post "register" do
required_attributes! [:token]
- attributes = { description: params[:description],
- tag_list: params[:tag_list] }
-
- unless params[:run_untagged].nil?
- attributes[:run_untagged] = params[:run_untagged]
- end
+ attributes = attributes_for_keys(
+ [:description, :tag_list, :run_untagged, :locked]
+ )
runner =
if runner_registration_token_valid?
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 40a5d180fd0..ed86de819eb 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -2,19 +2,24 @@ module Ci
class GitlabCiYamlProcessor
class ValidationError < StandardError; end
+ include Gitlab::Ci::Config::Node::ValidationHelpers
+
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
- :dependencies, :before_script, :after_script, :variables]
+ :dependencies, :before_script, :after_script, :variables,
+ :environment]
ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
- ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when]
+ ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in]
- attr_reader :before_script, :after_script, :image, :services, :path, :cache
+ attr_reader :after_script, :image, :services, :path, :cache
def initialize(config, path = nil)
- @config = Gitlab::Ci::Config.new(config).to_hash
+ @ci_config = Gitlab::Ci::Config.new(config)
+ @config = @ci_config.to_hash
+
@path = path
initial_parsing
@@ -25,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
@@ -46,13 +54,12 @@ module Ci
job = @jobs[name.to_sym]
return [] unless job
- job.fetch(:variables, [])
+ job[:variables] || []
end
private
def initial_parsing
- @before_script = @config[:before_script] || []
@after_script = @config[:after_script]
@image = @config[:image]
@services = @config[:services]
@@ -80,13 +87,14 @@ module Ci
{
stage_idx: stages.index(job[:stage]),
stage: job[:stage],
- commands: [job[:before_script] || @before_script, job[:script]].flatten.join("\n"),
+ commands: [job[:before_script] || [@ci_config.before_script], job[:script]].flatten.compact.join("\n"),
tag_list: job[:tags] || [],
name: name,
only: job[:only],
except: job[:except],
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
+ environment: job[:environment],
options: {
image: job[:image] || @image,
services: job[:services] || @services,
@@ -99,6 +107,10 @@ module Ci
end
def validate!
+ unless @ci_config.valid?
+ raise ValidationError, @ci_config.errors.first
+ end
+
validate_global!
@jobs.each do |name, job|
@@ -109,10 +121,6 @@ module Ci
end
def validate_global!
- unless validate_array_of_strings(@before_script)
- raise ValidationError, "before_script should be an array of strings"
- end
-
unless @after_script.nil? || validate_array_of_strings(@after_script)
raise ValidationError, "after_script should be an array of strings"
end
@@ -196,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])
@@ -211,6 +219,10 @@ module Ci
if job[:when] && !job[:when].in?(%w[on_success on_failure always])
raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
end
+
+ if job[:environment] && !validate_environment(job[:environment])
+ raise ValidationError, "#{name} job: environment parameter #{Gitlab::Regex.environment_name_regex_message}"
+ end
end
def validate_job_script!(name, job)
@@ -282,6 +294,10 @@ module Ci
if job[:artifacts][:when] && !job[:artifacts][:when].in?(%w[on_success on_failure always])
raise ValidationError, "#{name} job: artifacts:when parameter should be on_success, on_failure or always"
end
+
+ if job[:artifacts][:expire_in] && !validate_duration(job[:artifacts][:expire_in])
+ raise ValidationError, "#{name} job: artifacts:expire_in parameter should be a duration"
+ end
end
def validate_job_dependencies!(name, job)
@@ -300,22 +316,6 @@ module Ci
end
end
- def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? { |value| validate_string(value) }
- end
-
- def validate_variables(variables)
- variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) }
- end
-
- def validate_string(value)
- value.is_a?(String) || value.is_a?(Symbol)
- end
-
- def validate_boolean(value)
- value.in?([true, false])
- end
-
def process?(only_params, except_params, ref, tag, trigger_request)
if only_params.present?
return false unless matching?(only_params, ref, tag, trigger_request)
diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb
index 4e20dc4f875..eb5a2596177 100644
--- a/lib/container_registry/blob.rb
+++ b/lib/container_registry/blob.rb
@@ -18,7 +18,7 @@ module ContainerRegistry
end
def digest
- config['digest']
+ config['digest'] || config['blobSum']
end
def type
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 4d726692f45..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,7 +47,10 @@ module ContainerRegistry
conn.request :json
conn.headers['Accept'] = MANIFEST_VERSION
- conn.response :json, content_type: /\bjson$/
+ 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'
if options[:user] && options[:password]
conn.request(:basic_auth, options[:user].to_s, options[:password].to_s)
@@ -57,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/container_registry/tag.rb b/lib/container_registry/tag.rb
index 43f8d6dc8c2..708d01b95a1 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -3,6 +3,7 @@ module ContainerRegistry
attr_reader :repository, :name
delegate :registry, :client, to: :repository
+ delegate :revision, :short_revision, to: :config_blob, allow_nil: true
def initialize(repository, name)
@repository, @name = repository, name
@@ -12,6 +13,14 @@ module ContainerRegistry
manifest.present?
end
+ def v1?
+ manifest && manifest['schemaVersion'] == 1
+ end
+
+ def v2?
+ manifest && manifest['schemaVersion'] == 2
+ end
+
def manifest
return @manifest if defined?(@manifest)
@@ -57,7 +66,9 @@ module ContainerRegistry
return @layers if defined?(@layers)
return unless manifest
- @layers = manifest['layers'].map do |layer|
+ layers = manifest['layers'] || manifest['fsLayers']
+
+ @layers = layers.map do |layer|
repository.blob(layer)
end
end
@@ -65,7 +76,7 @@ module ContainerRegistry
def total_size
return unless layers
- layers.map(&:size).sum
+ layers.map(&:size).sum if v2?
end
def delete
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 6d0e30e916f..831f1e635ba 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -5,6 +5,8 @@
#
module Gitlab
module Access
+ class AccessDeniedError < StandardError; end
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index adbf5941a96..ab7b811c5d8 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)
@@ -33,7 +31,7 @@ module Grack
auth!
- lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
+ lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call
return lfs_response unless lfs_response.nil?
if @user.nil? && !@ci
@@ -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.rb b/lib/gitlab/ci/config.rb
index ffe633d4b63..b48d3592f16 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -1,11 +1,21 @@
module Gitlab
module Ci
+ ##
+ # Base GitLab CI Configuration facade
+ #
class Config
- class LoaderError < StandardError; end
+ delegate :valid?, :errors, to: :@global
+
+ ##
+ # Temporary delegations that should be removed after refactoring
+ #
+ delegate :before_script, to: :@global
def initialize(config)
- loader = Loader.new(config)
- @config = loader.load!
+ @config = Loader.new(config).load!
+
+ @global = Node::Global.new(@config)
+ @global.process!
end
def to_hash
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
new file mode 100644
index 00000000000..d60f87f3f94
--- /dev/null
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This mixin is responsible for adding DSL, which purpose is to
+ # simplifly process of adding child nodes.
+ #
+ # This can be used only if parent node is a configuration entry that
+ # holds a hash as a configuration value, for example:
+ #
+ # job:
+ # script: ...
+ # artifacts: ...
+ #
+ module Configurable
+ extend ActiveSupport::Concern
+
+ def allowed_nodes
+ self.class.allowed_nodes || {}
+ end
+
+ private
+
+ def prevalidate!
+ unless @value.is_a?(Hash)
+ @errors << 'should be a configuration entry with hash value'
+ end
+ end
+
+ def create_node(key, factory)
+ factory.with(value: @value[key])
+ factory.nullify! unless @value.has_key?(key)
+ factory.create!
+ end
+
+ class_methods do
+ def allowed_nodes
+ Hash[@allowed_nodes.map { |key, factory| [key, factory.dup] }]
+ end
+
+ private
+
+ def allow_node(symbol, entry_class, metadata)
+ factory = Node::Factory.new(entry_class)
+ .with(description: metadata[:description])
+
+ define_method(symbol) do
+ raise Entry::InvalidError unless valid?
+
+ @nodes[symbol].try(:value)
+ end
+
+ (@allowed_nodes ||= {}).merge!(symbol => factory)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
new file mode 100644
index 00000000000..52758a962f3
--- /dev/null
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Base abstract class for each configuration entry node.
+ #
+ class Entry
+ class InvalidError < StandardError; end
+
+ attr_accessor :description
+
+ def initialize(value)
+ @value = value
+ @nodes = {}
+ @errors = []
+
+ prevalidate!
+ end
+
+ def process!
+ return if leaf?
+ return unless valid?
+
+ compose!
+
+ nodes.each(&:process!)
+ nodes.each(&:validate!)
+ end
+
+ def nodes
+ @nodes.values
+ end
+
+ def valid?
+ errors.none?
+ end
+
+ def leaf?
+ allowed_nodes.none?
+ end
+
+ def errors
+ @errors + nodes.map(&:errors).flatten
+ end
+
+ def allowed_nodes
+ {}
+ end
+
+ def validate!
+ raise NotImplementedError
+ end
+
+ def value
+ raise NotImplementedError
+ end
+
+ private
+
+ def prevalidate!
+ end
+
+ def compose!
+ allowed_nodes.each do |key, essence|
+ @nodes[key] = create_node(key, essence)
+ end
+ end
+
+ def create_node(key, essence)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
new file mode 100644
index 00000000000..787ca006f5a
--- /dev/null
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Factory class responsible for fabricating node entry objects.
+ #
+ # It uses Fluent Interface pattern to set all necessary attributes.
+ #
+ class Factory
+ class InvalidFactory < StandardError; end
+
+ def initialize(entry_class)
+ @entry_class = entry_class
+ @attributes = {}
+ end
+
+ def with(attributes)
+ @attributes.merge!(attributes)
+ self
+ end
+
+ def nullify!
+ @entry_class = Node::Null
+ self
+ end
+
+ def create!
+ raise InvalidFactory unless @attributes.has_key?(:value)
+
+ @entry_class.new(@attributes[:value]).tap do |entry|
+ entry.description = @attributes[:description]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
new file mode 100644
index 00000000000..044603423d5
--- /dev/null
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This class represents a global entry - root node for entire
+ # GitLab CI Configuration file.
+ #
+ class Global < Entry
+ include Configurable
+
+ allow_node :before_script, Script,
+ description: 'Script that will be executed before each job.'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb
new file mode 100644
index 00000000000..4f590f6bec8
--- /dev/null
+++ b/lib/gitlab/ci/config/node/null.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This class represents a configuration entry that is not being used
+ # in configuration file.
+ #
+ # This implements Null Object pattern.
+ #
+ class Null < Entry
+ def value
+ nil
+ end
+
+ def validate!
+ nil
+ end
+
+ def method_missing(*)
+ nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/node/script.rb
new file mode 100644
index 00000000000..5072bf0db7d
--- /dev/null
+++ b/lib/gitlab/ci/config/node/script.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a script.
+ #
+ # Each element in the value array is a command that will be executed
+ # by GitLab Runner. Currently we concatenate these commands with
+ # new line character as a separator, what is compatible with
+ # implementation in Runner.
+ #
+ class Script < Entry
+ include ValidationHelpers
+
+ def value
+ @value.join("\n")
+ end
+
+ def validate!
+ unless validate_array_of_strings(@value)
+ @errors << 'before_script should be an array of strings'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/validation_helpers.rb b/lib/gitlab/ci/config/node/validation_helpers.rb
new file mode 100644
index 00000000000..72f648975dc
--- /dev/null
+++ b/lib/gitlab/ci/config/node/validation_helpers.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ module ValidationHelpers
+ private
+
+ def validate_duration(value)
+ value.is_a?(String) && ChronicDuration.parse(value)
+ rescue ChronicDuration::DurationParseError
+ false
+ end
+
+ def validate_array_of_strings(values)
+ 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) }
+ end
+
+ def validate_string(value)
+ 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
+
+ def validate_boolean(value)
+ value.in?([true, false])
+ end
+ end
+ end
+ end
+ end
+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.rb b/lib/gitlab/database.rb
index 04fa6a3a5de..078609c86f1 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -1,5 +1,10 @@
module Gitlab
module Database
+ # The max value of INTEGER type is the same between MySQL and PostgreSQL:
+ # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
+ # http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
+ MAX_INT_VALUE = 2147483647
+
def self.adapter_name
connection.adapter_name
end
@@ -30,6 +35,10 @@ module Gitlab
order
end
+ def self.random
+ Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
+ end
+
def true_value
if Gitlab::Database.postgresql?
"'t'"
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/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index e2fee6b9f3e..047c77c6fc2 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -37,7 +37,7 @@ module Gitlab
end
def diffs
- @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare)
+ @diffs ||= (safe_diff_files(compare.diffs(max_files: 30), diff_refs) if compare)
end
def diffs_count
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index e5cf66a0371..2286ac8829c 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -66,8 +66,7 @@ module Gitlab
end
def import_pull_requests
- hooks = client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?)
- disable_webhooks(hooks)
+ disable_webhooks
pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?)
@@ -90,14 +89,14 @@ module Gitlab
raise Projects::ImportService::Error, e.message
ensure
clean_up_restored_branches(branches_removed)
- clean_up_disabled_webhooks(hooks)
+ clean_up_disabled_webhooks
end
- def disable_webhooks(hooks)
+ def disable_webhooks
update_webhooks(hooks, active: false)
end
- def clean_up_disabled_webhooks(hooks)
+ def clean_up_disabled_webhooks
update_webhooks(hooks, active: true)
end
@@ -107,6 +106,20 @@ module Gitlab
end
end
+ def hooks
+ @hooks ||=
+ begin
+ client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?)
+
+ # The GitHub Repository Webhooks API returns 404 for users
+ # without admin access to the repository when listing hooks.
+ # In this case we just want to return gracefully instead of
+ # spitting out an error and stop the import process.
+ rescue Octokit::NotFound
+ []
+ end
+ end
+
def restore_branches(branches)
branches.each do |name, sha|
client.create_ref(repo, "refs/heads/#{name}", sha)
diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb
deleted file mode 100644
index f46b43b61a4..00000000000
--- a/lib/gitlab/gitignore.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-module Gitlab
- class Gitignore
- FILTER_REGEX = /\.gitignore\z/.freeze
-
- def initialize(path)
- @path = path
- end
-
- def name
- File.basename(@path, '.gitignore')
- end
-
- def content
- File.read(@path)
- end
-
- class << self
- def all
- languages_frameworks + global
- end
-
- def find(key)
- file_name = "#{key}.gitignore"
-
- directory = select_directory(file_name)
- directory ? new(File.join(directory, file_name)) : nil
- end
-
- def global
- files_for_folder(global_dir).map { |file| new(File.join(global_dir, file)) }
- end
-
- def languages_frameworks
- files_for_folder(gitignore_dir).map { |file| new(File.join(gitignore_dir, file)) }
- end
-
- private
-
- def select_directory(file_name)
- [gitignore_dir, global_dir].find { |dir| File.exist?(File.join(dir, file_name)) }
- end
-
- def global_dir
- File.join(gitignore_dir, 'Global')
- end
-
- def gitignore_dir
- Rails.root.join('vendor/gitignore')
- end
-
- def files_for_folder(dir)
- Dir.glob("#{dir.to_s}/*.gitignore").map { |file| file.gsub(FILTER_REGEX, '') }
- end
- end
- end
-end
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..99cf85d9a3b
--- /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
+ Rails.root.join('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/lfs/response.rb b/lib/gitlab/lfs/response.rb
index 9d9617761b3..e3ed2f6791d 100644
--- a/lib/gitlab/lfs/response.rb
+++ b/lib/gitlab/lfs/response.rb
@@ -2,10 +2,11 @@ module Gitlab
module Lfs
class Response
- def initialize(project, user, request)
+ def initialize(project, user, ci, request)
@origin_project = project
@project = storage_project(project)
@user = user
+ @ci = ci
@env = request.env
@request = request
end
@@ -189,7 +190,7 @@ module Gitlab
return render_not_enabled unless Gitlab.config.lfs.enabled
unless @project.public?
- return render_unauthorized unless @user
+ return render_unauthorized unless @user || @ci
return render_forbidden unless user_can_fetch?
end
@@ -210,7 +211,7 @@ module Gitlab
def user_can_fetch?
# Check user access against the project they used to initiate the pull
- @user.can?(:download_code, @origin_project)
+ @ci || @user.can?(:download_code, @origin_project)
end
def user_can_push?
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
index 78d02891102..69bd5e62305 100644
--- a/lib/gitlab/lfs/router.rb
+++ b/lib/gitlab/lfs/router.rb
@@ -1,9 +1,12 @@
module Gitlab
module Lfs
class Router
- def initialize(project, user, request)
+ attr_reader :project, :user, :ci, :request
+
+ def initialize(project, user, ci, request)
@project = project
@user = user
+ @ci = ci
@env = request.env
@request = request
end
@@ -80,7 +83,7 @@ module Gitlab
def lfs
return unless @project
- Gitlab::Lfs::Response.new(@project, @user, @request)
+ Gitlab::Lfs::Response.new(@project, @user, @ci, @request)
end
def sanitize_tmp_filename(name)
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 0f115893a15..dcec7543c13 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -56,7 +56,7 @@ module Gitlab
end
end
- # Instruments all public methods of a module.
+ # Instruments all public and private methods of a module.
#
# This method optionally takes a block that can be used to determine if a
# method should be instrumented or not. The block is passed the receiving
@@ -65,7 +65,8 @@ module Gitlab
#
# mod - The module to instrument.
def self.instrument_methods(mod)
- mod.public_methods(false).each do |name|
+ methods = mod.methods(false) + mod.private_methods(false)
+ methods.each do |name|
method = mod.method(name)
if method.owner == mod.singleton_class
@@ -76,13 +77,14 @@ module Gitlab
end
end
- # Instruments all public instance methods of a module.
+ # Instruments all public and private instance methods of a module.
#
# See `instrument_methods` for more information.
#
# mod - The module to instrument.
def self.instrument_instance_methods(mod)
- mod.public_instance_methods(false).each do |name|
+ methods = mod.instance_methods(false) + mod.private_instance_methods(false)
+ methods.each do |name|
method = mod.instance_method(name)
if method.owner == mod
@@ -146,20 +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
- retval = super
- duration = (Time.now - start) * 1000.0
-
- if duration >= Gitlab::Metrics.method_call_threshold
- trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
- { duration: 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 6f179789d3e..e61670f491c 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -1,8 +1,9 @@
module Gitlab
module Metrics
- # Rack middleware for tracking Rails requests.
+ # Rack middleware for tracking Rails and Grape requests.
class RackMiddleware
CONTROLLER_KEY = 'action_controller.instance'
+ ENDPOINT_KEY = 'api.endpoint'
def initialize(app)
@app = app
@@ -21,6 +22,8 @@ module Gitlab
ensure
if env[CONTROLLER_KEY]
tag_controller(trans, env)
+ elsif env[ENDPOINT_KEY]
+ tag_endpoint(trans, env)
end
trans.finish
@@ -32,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
@@ -42,6 +45,30 @@ module Gitlab
controller = env[CONTROLLER_KEY]
trans.action = "#{controller.class.name}##{controller.action_name}"
end
+
+ def tag_endpoint(trans, env)
+ endpoint = env[ENDPOINT_KEY]
+ path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path]
+ trans.action = "Grape##{endpoint.route.route_method} #{path}"
+ end
+
+ 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|
+ inner_hash[raw_path] = endpoint_instrumentable_path(raw_path)
+ end
+ end
+ end
+
+ def endpoint_instrumentable_path(raw_path)
+ raw_path.sub('(.:format)', '').sub('/:version', '')
+ end
end
end
end
diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb
index fc709222a9b..0000450d9bb 100644
--- a/lib/gitlab/metrics/sampler.rb
+++ b/lib/gitlab/metrics/sampler.rb
@@ -66,7 +66,11 @@ module Gitlab
def sample_objects
sample = Allocations.to_hash
counts = sample.each_with_object({}) do |(klass, count), hash|
- hash[klass.name] = count
+ name = klass.name
+
+ next unless name
+
+ hash[name] = count
end
# Symbols aren't allocated so we'll need to add those manually.
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/regex.rb b/lib/gitlab/regex.rb
index 1cbd6d945a0..c84c68f96f6 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -100,5 +100,13 @@ module Gitlab
def container_registry_reference_regex
git_reference_regex
end
+
+ def environment_name_regex
+ @environment_name_regex ||= /\A[a-zA-Z0-9_-]+\z/.freeze
+ end
+
+ def environment_name_regex_message
+ "can contain only letters, digits, '-' and '_'."
+ end
end
end
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
new file mode 100644
index 00000000000..760ff3e614a
--- /dev/null
+++ b/lib/gitlab/template/base_template.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ module Template
+ class BaseTemplate
+ def initialize(path)
+ @path = path
+ end
+
+ def name
+ File.basename(@path, self.class.extension)
+ end
+
+ def content
+ File.read(@path)
+ end
+
+ class << self
+ def all
+ self.categories.keys.flat_map { |cat| by_category(cat) }
+ end
+
+ def find(key)
+ file_name = "#{key}#{self.extension}"
+
+ directory = select_directory(file_name)
+ directory ? new(File.join(category_directory(directory), file_name)) : nil
+ end
+
+ def categories
+ raise NotImplementedError
+ end
+
+ def extension
+ raise NotImplementedError
+ end
+
+ def base_dir
+ raise NotImplementedError
+ end
+
+ def by_category(category)
+ templates_for_directory(category_directory(category))
+ end
+
+ def category_directory(category)
+ File.join(base_dir, categories[category])
+ end
+
+ private
+
+ def select_directory(file_name)
+ categories.keys.find do |category|
+ File.exist?(File.join(category_directory(category), file_name))
+ end
+ end
+
+ def templates_for_directory(dir)
+ dir << '/' unless dir.end_with?('/')
+ Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
+ end
+
+ def filter_regex
+ @filter_reges ||= /#{Regexp.escape(extension)}\z/
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore.rb
new file mode 100644
index 00000000000..964fbfd4de3
--- /dev/null
+++ b/lib/gitlab/template/gitignore.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Template
+ class Gitignore < BaseTemplate
+ class << self
+ def extension
+ '.gitignore'
+ end
+
+ def categories
+ {
+ "Languages" => '',
+ "Global" => 'Global'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitignore')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml.rb
new file mode 100644
index 00000000000..7f480fe33c0
--- /dev/null
+++ b/lib/gitlab/template/gitlab_ci_yml.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Template
+ class GitlabCiYml < BaseTemplate
+ def content
+ explanation = "# This file is a template, and might need editing before it works on your project."
+ [explanation, super].join("\n")
+ end
+
+ class << self
+ def extension
+ '.gitlab-ci.yml'
+ end
+
+ def categories
+ {
+ "General" => '',
+ "Pages" => 'Pages'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitlab-ci-yml')
+ end
+ 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/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
new file mode 100644
index 00000000000..c2c6031db67
--- /dev/null
+++ b/lib/tasks/gitlab/import_export.rake
@@ -0,0 +1,13 @@
+namespace :gitlab do
+ namespace :import_export do
+ desc "GitLab | Show Import/Export version"
+ task version: :environment do
+ puts "Import/Export v#{Gitlab::ImportExport.version}"
+ end
+
+ desc "GitLab | Display exported DB structure"
+ task data: :environment do
+ puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(:SortKeys => true)
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake
deleted file mode 100644
index 4fd48cccb1d..00000000000
--- a/lib/tasks/gitlab/update_gitignore.rake
+++ /dev/null
@@ -1,46 +0,0 @@
-namespace :gitlab do
- desc "GitLab | Update gitignore"
- task :update_gitignore do
- unless clone_gitignores
- puts "Cloning the gitignores failed".color(:red)
- return
- end
-
- remove_unneeded_files(gitignore_directory)
- remove_unneeded_files(global_directory)
-
- puts "Done".color(:green)
- end
-
- def clone_gitignores
- FileUtils.rm_rf(gitignore_directory) if Dir.exist?(gitignore_directory)
- FileUtils.cd vendor_directory
-
- system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git')
- end
-
- # Retain only certain files:
- # - The LICENSE, because we have to
- # - The sub dir global
- # - The gitignores themself
- # - Dir.entires returns also the entries '.' and '..'
- def remove_unneeded_files(path)
- Dir.foreach(path) do |file|
- FileUtils.rm_rf(File.join(path, file)) unless file =~ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/
- end
- end
-
- private
-
- def vendor_directory
- Rails.root.join('vendor')
- end
-
- def gitignore_directory
- File.join(vendor_directory, 'gitignore')
- end
-
- def global_directory
- File.join(gitignore_directory, 'Global')
- end
-end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
new file mode 100644
index 00000000000..4f76dad7286
--- /dev/null
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -0,0 +1,54 @@
+namespace :gitlab do
+ desc "GitLab | Update templates"
+ task :update_templates do
+ TEMPLATE_DATA.each { |template| update(template) }
+ end
+
+ def update(template)
+ sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1]
+ dir = File.join(vendor_directory, sub_dir)
+
+ unless clone_repository(template.repo_url, dir)
+ puts "Cloning the #{sub_dir} templates failed".red
+ return
+ end
+
+ remove_unneeded_files(dir, template.cleanup_regex)
+ puts "Done".green
+ end
+
+ def clone_repository(url, directory)
+ FileUtils.rm_rf(directory) if Dir.exist?(directory)
+
+ system("git clone #{url} --depth=1 --branch=master #{directory}")
+ end
+
+ # Retain only certain files:
+ # - The LICENSE, because we have to
+ # - The sub dirs so we can organise the file by category
+ # - The templates themself
+ # - Dir.entries returns also the entries '.' and '..'
+ def remove_unneeded_files(directory, regex)
+ Dir.foreach(directory) do |file|
+ FileUtils.rm_rf(File.join(directory, file)) unless file =~ regex
+ end
+ end
+
+ private
+
+ Template = Struct.new(:repo_url, :cleanup_regex)
+ TEMPLATE_DATA = [
+ Template.new(
+ "https://github.com/github/gitignore.git",
+ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/
+ ),
+ Template.new(
+ "https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
+ /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/
+ )
+ ]
+
+ def vendor_directory
+ Rails.root.join('vendor')
+ end
+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/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb
index eb91e577b87..465013231f9 100644
--- a/spec/controllers/blob_controller_spec.rb
+++ b/spec/controllers/blob_controller_spec.rb
@@ -38,6 +38,11 @@ describe Projects::BlobController do
let(:id) { 'invalid-branch/README.md' }
it { is_expected.to respond_with(:not_found) }
end
+
+ context "binary file" do
+ let(:id) { 'binary-encoding/encoding/binary-1.bin' }
+ it { is_expected.to respond_with(:success) }
+ end
end
describe 'GET show with tree path' do
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index a5986598715..c8601341d54 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -4,17 +4,209 @@ describe Groups::GroupMembersController do
let(:user) { create(:user) }
let(:group) { create(:group) }
- context "index" do
+ describe '#index' do
before do
group.add_owner(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'renders index with group members' do
- get :index, group_id: group.path
+ get :index, group_id: group
expect(response.status).to eq(200)
expect(response).to render_template(:index)
end
end
+
+ describe '#destroy' do
+ let(:group) { create(:group, :public) }
+
+ context 'when member is not found' do
+ it 'returns 403' do
+ delete :destroy, group_id: group,
+ id: 42
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:member) do
+ group.add_developer(group_user)
+ group.members.find_by(user_id: group_user)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'returns 403' do
+ delete :destroy, group_id: group,
+ id: member
+
+ expect(response.status).to eq(403)
+ expect(group.users).to include group_user
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it '[HTML] removes user from members' do
+ delete :destroy, group_id: group,
+ id: member
+
+ expect(response).to set_flash.to 'User was successfully removed from group.'
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).not_to include group_user
+ end
+
+ it '[JS] removes user from members' do
+ xhr :delete, :destroy, group_id: group,
+ id: member
+
+ expect(response).to be_success
+ expect(group.users).not_to include group_user
+ end
+ end
+ end
+ end
+
+ describe '#leave' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ context 'when member is not found' do
+ before { sign_in(user) }
+
+ it 'returns 403' do
+ delete :leave, group_id: group
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ context 'and is not an owner' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, group_id: group
+
+ expect(response).to set_flash.to "You left the \"#{group.name}\" group."
+ expect(response).to redirect_to(dashboard_groups_path)
+ expect(group.users).not_to include user
+ end
+ end
+
+ context 'and is an owner' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'cannot removes himself from the group' do
+ delete :leave, group_id: group
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'and is a requester' do
+ before do
+ group.request_access(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, group_id: group
+
+ expect(response).to set_flash.to 'Your access request to the group has been withdrawn.'
+ expect(response).to redirect_to(group_path(group))
+ expect(group.members.request).to be_empty
+ expect(group.users).not_to include user
+ end
+ end
+ end
+ end
+
+ describe '#request_access' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates a new GroupMember that is not a team member' do
+ post :request_access, group_id: group
+
+ expect(response).to set_flash.to 'Your request for access has been queued for review.'
+ expect(response).to redirect_to(group_path(group))
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+ expect(group.users).not_to include user
+ end
+ end
+
+ describe '#approve_access_request' do
+ let(:group) { create(:group, :public) }
+
+ context 'when member is not found' do
+ it 'returns 403' do
+ post :approve_access_request, group_id: group,
+ id: 42
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:group_requester) { create(:user) }
+ let(:member) do
+ group.request_access(group_requester)
+ group.members.request.find_by(user_id: group_requester)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'returns 403' do
+ post :approve_access_request, group_id: group,
+ id: member
+
+ expect(response.status).to eq(403)
+ expect(group.users).not_to include group_requester
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'adds user to members' do
+ post :approve_access_request, group_id: group,
+ id: member
+
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).to include group_requester
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb
new file mode 100644
index 00000000000..4eafc11abaa
--- /dev/null
+++ b/spec/controllers/profiles/accounts_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Profiles::AccountsController do
+
+ let(:user) { create(:omniauth_user, provider: 'saml') }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'does not allow to unlink SAML connected account' do
+ identity = user.identities.last
+ delete :unlink, provider: 'saml'
+ updated_user = User.find(user.id)
+
+ expect(response.status).to eq(302)
+ expect(updated_user.identities.size).to eq(1)
+ expect(updated_user.identities).to include(identity)
+ end
+
+ it 'does allow to delete other linked accounts' do
+ user.identities.create(provider: 'twitter', extern_uid: 'twitter_123')
+
+ expect { delete :unlink, provider: 'twitter' }.to change(Identity.all, :size).by(-1)
+ end
+end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 438e776ec4b..6e3db10e451 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -2,6 +2,8 @@ require 'rails_helper'
describe Projects::CommitController do
describe 'GET show' do
+ render_views
+
let(:project) { create(:project) }
before do
@@ -27,6 +29,16 @@ describe Projects::CommitController do
end
end
+ it 'handles binary files' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: TestEnv::BRANCH_SHA['binary-encoding'],
+ format: "html")
+
+ expect(response).to be_success
+ end
+
def go(id:)
get :show,
namespace_id: project.namespace.to_param,
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 78be7e3dc35..cbaa3e0b7b2 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -105,6 +105,15 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to eq [issue]
end
+ it 'should not list confidential issues for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
it 'should list confidential issues for author' do
sign_in(author)
get_issues
@@ -148,7 +157,7 @@ describe Projects::IssuesController do
shared_examples_for 'restricted action' do |http_status|
it 'returns 404 for guests' do
- sign_out :user
+ sign_out(:user)
go(id: unescaped_parameter_value.to_param)
expect(response).to have_http_status :not_found
@@ -161,6 +170,14 @@ describe Projects::IssuesController do
expect(response).to have_http_status :not_found
end
+ it 'returns 404 for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
it "returns #{http_status[:success]} for author" do
sign_in(author)
go(id: unescaped_parameter_value.to_param)
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 750fbecdd07..e5e750c855f 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -1,22 +1,22 @@
require('spec_helper')
describe Projects::ProjectMembersController do
- let(:project) { create(:project) }
- let(:another_project) { create(:project, :private) }
- let(:user) { create(:user) }
- let(:member) { create(:user) }
-
- before do
- project.team << [user, :master]
- another_project.team << [member, :guest]
- sign_in(user)
- end
-
describe '#apply_import' do
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project, :private) }
+ let(:user) { create(:user) }
+ let(:member) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ another_project.team << [member, :guest]
+ sign_in(user)
+ end
+
shared_context 'import applied' do
before do
- post(:apply_import, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:apply_import, namespace_id: project.namespace,
+ project_id: project,
source_project_id: another_project.id)
end
end
@@ -48,18 +48,227 @@ describe Projects::ProjectMembersController do
end
describe '#index' do
- let(:project) { create(:project, :private) }
-
context 'when user is member' do
- let(:member) { create(:user) }
-
before do
+ project = create(:project, :private)
+ member = create(:user)
project.team << [member, :guest]
sign_in(member)
- get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ get :index, namespace_id: project.namespace, project_id: project
end
it { expect(response.status).to eq(200) }
end
end
+
+ describe '#destroy' do
+ let(:project) { create(:project, :public) }
+
+ context 'when member is not found' do
+ it 'returns 404' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: 42
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:team_user) { create(:user) }
+ let(:member) do
+ project.team << [team_user, :developer]
+ project.members.find_by(user_id: team_user.id)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response.status).to eq(404)
+ expect(project.users).to include team_user
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it '[HTML] removes user from members' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ expect(project.users).not_to include team_user
+ end
+
+ it '[JS] removes user from members' do
+ xhr :delete, :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to be_success
+ expect(project.users).not_to include team_user
+ end
+ end
+ end
+ end
+
+ describe '#leave' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ context 'when member is not found' do
+ before { sign_in(user) }
+
+ it 'returns 403' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ context 'and is not an owner' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to "You left the \"#{project.human_name}\" project."
+ expect(response).to redirect_to(dashboard_projects_path)
+ expect(project.users).not_to include user
+ end
+ end
+
+ context 'and is an owner' do
+ before do
+ project.update(namespace_id: user.namespace_id)
+ project.team << [user, :master, user]
+ sign_in(user)
+ end
+
+ it 'cannot remove himself from the project' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'and is a requester' do
+ before do
+ project.request_access(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to 'Your access request to the project has been withdrawn.'
+ expect(response).to redirect_to(namespace_project_path(project.namespace, project))
+ expect(project.members.request).to be_empty
+ expect(project.users).not_to include user
+ end
+ end
+ end
+ end
+
+ describe '#request_access' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates a new ProjectMember that is not a team member' do
+ post :request_access, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to 'Your request for access has been queued for review.'
+ expect(response).to redirect_to(
+ namespace_project_path(project.namespace, project)
+ )
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+ expect(project.users).not_to include user
+ end
+ end
+
+ describe '#approve' do
+ let(:project) { create(:project, :public) }
+
+ context 'when member is not found' do
+ it 'returns 404' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: 42
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:team_requester) { create(:user) }
+ let(:member) do
+ project.request_access(team_requester)
+ project.members.request.find_by(user_id: team_requester.id)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response.status).to eq(404)
+ expect(project.users).not_to include team_requester
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it 'adds user to members' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ expect(project.users).to include team_requester
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
new file mode 100644
index 00000000000..40a3403b660
--- /dev/null
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -0,0 +1,102 @@
+require('spec_helper')
+
+describe Projects::TodosController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ context 'Issues' do
+ describe 'POST create' do
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'should create todo for issue' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(1)
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not authorized' do
+ it 'should not create todo for issue that user has no access to' do
+ sign_in(user)
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'should not create todo for issue when user not logged in' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(302)
+ end
+ end
+ end
+ end
+
+ context 'Merge Requests' do
+ describe 'POST create' do
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'should create todo for merge request' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(1)
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not authorized' do
+ it 'should not create todo for merge request user has no access to' do
+ sign_in(user)
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'should not create todo for merge request user has no access to' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(302)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index fba545560c7..146b2c2e131 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -237,4 +237,24 @@ describe ProjectsController do
expect(response.status).to eq(401)
end
end
+
+ describe "GET refs" do
+ it "should get a list of branches and tags" do
+ get :refs, namespace_id: public_project.namespace.path, id: public_project.path
+
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body["Branches"]).to include("master")
+ expect(parsed_body["Tags"]).to include("v1.0.0")
+ expect(parsed_body["Commits"]).to be_nil
+ end
+
+ it "should get a list of branches, tags and commits" do
+ get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
+
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body["Branches"]).to include("master")
+ expect(parsed_body["Tags"]).to include("v1.0.0")
+ expect(parsed_body["Commits"]).to include("123456")
+ end
+ end
end
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/pipelines.rb
index a039bef6f3c..a039bef6f3c 100644
--- a/spec/factories/ci/commits.rb
+++ b/spec/factories/ci/pipelines.rb
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
new file mode 100644
index 00000000000..82591604fcb
--- /dev/null
+++ b/spec/factories/deployments.rb
@@ -0,0 +1,13 @@
+FactoryGirl.define do
+ factory :deployment, class: Deployment do
+ sha '97de212e80737a608d939f648d959671fb0a0142'
+ ref 'master'
+ tag false
+
+ environment factory: :environment
+
+ after(:build) do |deployment, evaluator|
+ deployment.project = deployment.environment.project
+ end
+ end
+end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
new file mode 100644
index 00000000000..07265c26ca3
--- /dev/null
+++ b/spec/factories/environments.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :environment, class: Environment do
+ sequence(:name) { |n| "environment#{n}" }
+
+ project factory: :empty_project
+ 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/factories/projects.rb b/spec/factories/projects.rb
index da8d97c9f82..5c8ddbebf0d 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -67,9 +67,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
}
)
-
- project.issues_tracker = 'redmine'
- project.issues_tracker_id = 'project_name_in_redmine'
end
end
@@ -84,9 +81,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
}
)
-
- project.issues_tracker = 'jira'
- project.issues_tracker_id = 'project_name_in_jira'
end
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/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 9499cd4e025..2d297776cb0 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -60,6 +60,40 @@ describe "Admin Runners" do
it { expect(page).to have_content(@project1.name_with_namespace) }
it { expect(page).not_to have_content(@project2.name_with_namespace) }
end
+
+ describe 'enable/create' do
+ before do
+ @project1.runners << runner
+ visit admin_runner_path(runner)
+ end
+
+ it 'enables specific runner for project' do
+ within '.unassigned-projects' do
+ click_on 'Enable'
+ end
+
+ assigned_project = page.find('.assigned-projects')
+
+ expect(assigned_project).to have_content(@project2.path)
+ end
+ end
+
+ describe 'disable/destroy' do
+ before do
+ @project1.runners << runner
+ visit admin_runner_path(runner)
+ end
+
+ it 'enables specific runner for project' do
+ within '.assigned-projects' do
+ click_on 'Disable'
+ end
+
+ new_runner_project = page.find('.unassigned-projects')
+
+ expect(new_runner_project).to have_content(@project1.path)
+ end
+ end
end
describe 'runners registration token' do
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index b710cb3c72f..4dd9548cfc5 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -5,8 +5,6 @@ describe "Dashboard Issues Feed", feature: true do
let!(:user) { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }
- let!(:issue1) { create(:issue, author: user, assignee: user, project: project1) }
- let!(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
before do
project1.team << [user, :master]
@@ -14,16 +12,51 @@ describe "Dashboard Issues Feed", feature: true do
end
describe "atom feed" do
- it "should render atom feed via private token" do
+ it "renders atom feed via private token" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
- expect(response_headers['Content-Type']).
- to have_content('application/atom+xml')
+ expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
- expect(body).to have_selector('author email', text: issue1.author_email)
- expect(body).to have_selector('entry summary', text: issue1.title)
- expect(body).to have_selector('author email', text: issue2.author_email)
- expect(body).to have_selector('entry summary', text: issue2.title)
+ end
+
+ context "issue with basic fields" do
+ let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+
+ it "renders issue fields" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue2.author_email)
+ expect(entry).to have_selector('assignee email', text: issue2.author_email)
+ expect(entry).not_to have_selector('labels')
+ expect(entry).not_to have_selector('milestone')
+ expect(entry).to have_selector('description', text: issue2.description)
+ end
+ end
+
+ context "issue with label and milestone" do
+ let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
+ let!(:label1) { create(:label, project: project1, title: 'label1') }
+ let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+
+ before do
+ issue1.labels << label1
+ end
+
+ it "renders issue label and milestone info" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue1.author_email)
+ expect(entry).to have_selector('assignee email', text: issue1.author_email)
+ expect(entry).to have_selector('labels label', text: label1.title)
+ expect(entry).to have_selector('milestone', text: milestone1.title)
+ expect(entry).not_to have_selector('description')
+ end
end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index b8ecc356b4d..16832c297ac 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -97,6 +97,42 @@ describe "Builds" do
end
end
+ context 'Artifacts expire date' do
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at)
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ context 'no expire date defined' do
+ let(:expire_at) { nil }
+
+ it 'does not have the Keep button' do
+ expect(page).not_to have_content 'Keep'
+ end
+ end
+
+ context 'when expire date is defined' do
+ let(:expire_at) { Time.now + 7.days }
+
+ it 'keeps artifacts when Keep button is clicked' do
+ expect(page).to have_content 'The artifacts will be removed'
+ click_link 'Keep'
+
+ expect(page).not_to have_link 'Keep'
+ expect(page).not_to have_content 'The artifacts will be removed'
+ end
+ end
+
+ context 'when artifacts expired' do
+ let(:expire_at) { Time.now - 7.days }
+
+ it 'does not have the Keep button' do
+ expect(page).to have_content 'The artifacts were removed'
+ expect(page).not_to have_link 'Keep'
+ end
+ end
+ end
+
context 'Build raw trace' do
before do
@build.run!
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 53b4f027117..203e55a36f2 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -26,7 +26,8 @@ describe "Container Registry" do
end
context 'when there are tags' do
- it { expect(page).to have_content(tag_name)}
+ it { expect(page).to have_content(tag_name) }
+ it { expect(page).to have_content('d7a513a66') }
end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
new file mode 100644
index 00000000000..7fb28f4174b
--- /dev/null
+++ b/spec/features/environments_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+feature 'Environments', feature: true do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when showing environments' do
+ given!(:environment) { }
+ given!(:deployment) { }
+
+ before do
+ visit namespace_project_environments_path(project.namespace, project)
+ end
+
+ context 'without environments' do
+ scenario 'does show no environments' do
+ expect(page).to have_content('You don\'t have any environments right now.')
+ end
+ end
+
+ context 'with environments' do
+ given(:environment) { create(:environment, project: project) }
+
+ scenario 'does show environment name' do
+ expect(page).to have_link(environment.name)
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('No deployments yet')
+ end
+ end
+
+ context 'with deployments' do
+ given(:deployment) { create(:deployment, environment: environment) }
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ end
+ end
+ end
+
+ scenario 'does have a New environment button' do
+ expect(page).to have_link('New environment')
+ end
+ end
+
+ describe 'when showing the environment' do
+ given(:environment) { create(:environment, project: project) }
+ given!(:deployment) { }
+
+ before do
+ visit namespace_project_environment_path(project.namespace, project, environment)
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
+ context 'with deployments' do
+ given(:deployment) { create(:deployment, environment: environment) }
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ end
+
+ scenario 'does not show a retry button for deployment without build' do
+ expect(page).not_to have_link('Retry')
+ end
+
+ context 'with build' do
+ given(:build) { create(:ci_build, project: project) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show build name' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+
+ scenario 'does show retry button' do
+ expect(page).to have_link('Retry')
+ end
+ end
+ end
+ end
+
+ describe 'when creating a new environment' do
+ before do
+ visit namespace_project_environments_path(project.namespace, project)
+ end
+
+ context 'when logged as developer' do
+ before do
+ click_link 'New environment'
+ end
+
+ context 'for valid name' do
+ before do
+ fill_in('Name', with: 'production')
+ click_on 'Create environment'
+ end
+
+ scenario 'does create a new pipeline' do
+ expect(page).to have_content('Production')
+ end
+ end
+
+ context 'for invalid name' do
+ before do
+ fill_in('Name', with: 'name with spaces')
+ click_on 'Create environment'
+ end
+
+ scenario 'does show errors' do
+ expect(page).to have_content('Name can contain only letters')
+ end
+ end
+ end
+
+ context 'when logged as reporter' do
+ given(:role) { :reporter }
+
+ scenario 'does not have a New environment link' do
+ expect(page).not_to have_link('New environment')
+ end
+ end
+ end
+
+ describe 'when deleting existing environment' do
+ given(:environment) { create(:environment, project: project) }
+
+ before do
+ visit namespace_project_environment_path(project.namespace, project, environment)
+ end
+
+ context 'when logged as master' do
+ given(:role) { :master }
+
+ scenario 'does delete environment' do
+ click_link 'Destroy'
+ expect(page).not_to have_link(environment.name)
+ end
+ end
+
+ context 'when logged as developer' do
+ given(:role) { :developer }
+
+ scenario 'does not have a Destroy link' do
+ expect(page).not_to have_link('Destroy')
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
new file mode 100644
index 00000000000..33bf6d3752f
--- /dev/null
+++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Last owner cannot leave group', feature: true do
+ let(:owner) { create(:user) }
+ let(:group) { create(:group) }
+
+ background do
+ group.add_owner(owner)
+ login_as(owner)
+ visit group_path(group)
+ end
+
+ scenario 'user does not see a "Leave Group" link' do
+ expect(page).not_to have_content 'Leave Group'
+ end
+end
diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb
new file mode 100644
index 00000000000..3185ff924b9
--- /dev/null
+++ b/spec/features/groups/members/member_leaves_group_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Member leaves group', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.add_owner(owner)
+ group.add_developer(user)
+ login_as(user)
+ visit group_path(group)
+ end
+
+ scenario 'user leaves group' do
+ click_link 'Leave Group'
+
+ expect(current_path).to eq(dashboard_groups_path)
+ expect(group.users.exists?(user.id)).to be_falsey
+ end
+end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..321c9bad7d0
--- /dev/null
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Owner manages access requests', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.request_access(user)
+ group.add_owner(owner)
+ login_as(owner)
+ end
+
+ scenario 'owner can see access requests' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+ end
+
+ scenario 'master can grant access' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+
+ perform_enqueued_jobs { click_on 'Grant access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted"
+ end
+
+ scenario 'master can deny access' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+
+ perform_enqueued_jobs { click_on 'Deny access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was denied"
+ end
+
+
+ def expect_visible_access_request(group, user)
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "#{group.name} access requests 1"
+ expect(page).to have_content user.name
+ end
+end
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
new file mode 100644
index 00000000000..1ea607cbca0
--- /dev/null
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+feature 'Groups > Members > User requests access', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.add_owner(owner)
+ login_as(user)
+ visit group_path(group)
+ end
+
+ scenario 'user can request access to a group' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [owner.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Request to join the #{group.name} group"
+
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content 'Your request for access has been queued for review.'
+
+ expect(page).to have_content 'Withdraw Access Request'
+ expect(page).not_to have_content 'Leave Group'
+ end
+
+ scenario 'user is not listed in the group members page' do
+ click_link 'Request Access'
+
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+
+ click_link 'Members'
+
+ page.within('.content') do
+ expect(page).not_to have_content(user.name)
+ end
+ end
+
+ scenario 'user can withdraw its request for access' do
+ click_link 'Request Access'
+
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+
+ click_link 'Withdraw Access Request'
+
+ expect(group.members.request.exists?(user_id: user)).to be_falsey
+ expect(page).to have_content 'Your access request to the group has been withdrawn.'
+ end
+end
diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 0fbc2062e39..7143d0e40f3 100644
--- a/spec/features/issues/bulk_assigment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -190,7 +190,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
end
if unmark
items.map do |item|
- click_link item
+ # Make sure we are unmarking the item no matter the state it has currently
+ click_link item until find('a', text: item)[:class] == 'label-item'
end
end
end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 16c619c9288..5ea02b8d39c 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -56,8 +56,9 @@ feature 'Issue filtering by Labels', feature: true do
end
it 'should remove label "bug"' do
- first('.js-label-filter-remove').click
- expect(find('.filtered-labels')).to have_no_content "bug"
+ find('.js-label-filter-remove').click
+ wait_for_ajax
+ expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end
end
@@ -142,7 +143,8 @@ feature 'Issue filtering by Labels', feature: true do
end
it 'should remove label "enhancement"' do
- first('.js-label-filter-remove').click
+ find('.js-label-filter-remove', match: :first).click
+ wait_for_ajax
expect(find('.filtered-labels')).to have_no_content "enhancement"
end
end
@@ -179,6 +181,7 @@ feature 'Issue filtering by Labels', feature: true do
before do
page.within '.labels-filter' do
click_button 'Label'
+ wait_for_ajax
click_link 'bug'
find('.dropdown-menu-close').click
end
@@ -189,14 +192,11 @@ feature 'Issue filtering by Labels', feature: true do
end
it 'should allow user to remove filtered labels' do
- page.within '.filtered-labels' do
- first('.js-label-filter-remove').click
- expect(page).not_to have_content 'bug'
- end
+ first('.js-label-filter-remove').click
+ wait_for_ajax
- page.within '.labels-filter' do
- expect(page).not_to have_content 'bug'
- end
+ expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
+ expect(find('.labels-filter')).not_to have_content 'bug'
end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 1f0594e6b02..4bcb105b17d 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Filter issues', feature: true do
+ include WaitForAjax
let!(:project) { create(:project) }
let!(:user) { create(:user)}
@@ -21,7 +22,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- sleep 2
+ wait_for_ajax
end
context 'assignee', js: true do
@@ -53,7 +54,7 @@ describe 'Filter issues', feature: true do
find('.milestone-filter .dropdown-content a', text: milestone.title).click
- sleep 2
+ wait_for_ajax
end
context 'milestone', js: true do
@@ -80,23 +81,21 @@ describe 'Filter issues', feature: true do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click
+ wait_for_ajax
end
it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
- page.within '.labels-filter' do
- expect(page).to have_content 'Any Label'
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label')
+ expect(find('.labels-filter')).to have_content 'Label'
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
page.within '.labels-filter' do
expect(page).to have_content 'No Label'
@@ -122,14 +121,14 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- sleep 2
+ wait_for_ajax
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
end
context 'assignee and label', js: true do
@@ -276,9 +275,12 @@ describe 'Filter issues', feature: true do
it 'should be able to filter and sort issues' do
click_button 'Label'
+ wait_for_ajax
page.within '.labels-filter' do
click_link 'bug'
end
+ find('.dropdown-menu-close-icon').click
+ wait_for_ajax
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
@@ -288,6 +290,7 @@ describe 'Filter issues', feature: true do
page.within '.dropdown-menu-sort' do
click_link 'Oldest created'
end
+ wait_for_ajax
page.within '.issues-list' do
expect(first('.issue')).to have_content('Frontend')
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index c7019c5aea1..7773c486b4e 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -26,6 +26,7 @@ feature 'issue move to another project' do
context 'user has permission to move issue' do
let!(:mr) { create(:merge_request, source_project: old_project) }
let(:new_project) { create(:project) }
+ let(:new_project_search) { create(:project) }
let(:text) { 'Text with !1' }
let(:cross_reference) { old_project.to_reference }
@@ -47,6 +48,21 @@ feature 'issue move to another project' do
expect(page).to have_content(issue.title)
end
+ scenario 'searching project dropdown', js: true do
+ new_project_search.team << [user, :reporter]
+
+ page.within '.js-move-dropdown' do
+ first('.select2-choice').click
+ end
+
+ fill_in('s2id_autogen2_search', with: new_project_search.name)
+
+ page.within '.select2-drop' do
+ expect(page).to have_content(new_project_search.name)
+ expect(page).not_to have_content(new_project.name)
+ end
+ end
+
context 'user does not have permission to move the issue to a project', js: true do
let!(:private_project) { create(:project, :private) }
let(:another_project) { create(:project) }
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
new file mode 100644
index 00000000000..bc0f437a8ce
--- /dev/null
+++ b/spec/features/issues/todo_spec.rb
@@ -0,0 +1,43 @@
+require 'rails_helper'
+
+feature 'Manually create a todo item from issue', feature: true, js: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should create todo when clicking button' do
+ page.within '.issuable-sidebar' do
+ click_button 'Add Todo'
+ expect(page).to have_content 'Mark Done'
+ end
+
+ page.within '.header-content .todos-pending-count' do
+ expect(page).to have_content '1'
+ end
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ page.within '.header-content .todos-pending-count' do
+ expect(page).to have_content '1'
+ end
+ end
+
+ it 'should mark a todo as done' do
+ page.within '.issuable-sidebar' do
+ click_button 'Add Todo'
+ click_button 'Mark Done'
+ end
+
+ expect(page).to have_selector('.todos-pending-count', visible: false)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_selector('.todos-pending-count', visible: false)
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index f6fb6a72d22..5065dfb849c 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -22,7 +22,7 @@ describe 'Issues', feature: true do
before do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
- click_button "Go full screen"
+ find('.js-zen-enter').click
end
it 'should open new issue popup' do
@@ -396,6 +396,27 @@ describe 'Issues', feature: true do
expect(page).to have_content @user.name
end
end
+
+ it 'allows user to unselect themselves', js: true do
+ issue2 = create(:issue, project: project, author: @user)
+ visit namespace_project_issue_path(project.namespace, project, issue2)
+
+ page.within '.assignee' do
+ click_link 'Edit'
+ click_link @user.name
+
+ page.within '.value' do
+ expect(page).to have_content @user.name
+ end
+
+ click_link 'Edit'
+ click_link @user.name
+
+ page.within '.value' do
+ expect(page).to have_content "No assignee"
+ end
+ end
+ end
end
context 'by unauthorized user' do
@@ -440,6 +461,26 @@ describe 'Issues', feature: true do
expect(issue.reload.milestone).to be_nil
end
+
+ it 'allows user to de-select milestone', js: true do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ page.within('.milestone') do
+ click_link 'Edit'
+ click_link milestone.title
+
+ page.within '.value' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_link 'Edit'
+ click_link milestone.title
+
+ page.within '.value' do
+ expect(page).to have_content 'None'
+ end
+ end
+ end
end
context 'by unauthorized user' do
@@ -515,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/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 51be81d634c..01e90618a98 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'list of badges' do
- include Select2Helper
-
background do
user = create(:user)
project = create(:project)
@@ -24,7 +22,11 @@ feature 'list of badges' do
end
scenario 'user changes current ref on badges list page', js: true do
- select2('improve/awesome', from: '#ref')
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'improve/awesome'
+ end
expect(page).to have_content 'badges/improve/awesome/build.svg'
end
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
new file mode 100644
index 00000000000..d516e8ce55a
--- /dev/null
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+feature 'User wants to add a .gitlab-ci.yml file', feature: true do
+ include WaitForAjax
+
+ before do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitlab-ci.yml')
+ end
+
+ scenario 'user can see .gitlab-ci.yml dropdown' do
+ expect(page).to have_css('.gitlab-ci-yml-selector')
+ end
+
+ scenario 'user can pick a template from the dropdown', js: true do
+ find('.js-gitlab-ci-yml-selector').click
+ wait_for_ajax
+ within '.gitlab-ci-yml-selector' do
+ find('.dropdown-input-field').set('jekyll')
+ find('.dropdown-content li', text: 'jekyll').click
+ end
+ wait_for_ajax
+
+ expect(page).to have_content('This file is a template, and might need editing before it works on your project')
+ expect(page).to have_content('jekyll build -d test')
+ 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/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
new file mode 100644
index 00000000000..4d5d656f00c
--- /dev/null
+++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group member cannot request access to his group project', feature: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ background do
+ end
+
+ scenario 'owner does not see the request access button' do
+ group.add_owner(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'master does not see the request access button' do
+ group.add_master(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'developer does not see the request access button' do
+ group.add_developer(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'reporter does not see the request access button' do
+ group.add_reporter(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'guest does not see the request access button' do
+ group.add_guest(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ def login_and_visit_project_page(user)
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
new file mode 100644
index 00000000000..c4ed92d2780
--- /dev/null
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group requester cannot request access to project', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
+
+ background do
+ group.add_owner(owner)
+ login_as(user)
+ visit group_path(group)
+ perform_enqueued_jobs { click_link 'Request Access' }
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'group requester does not see the request access / withdraw access request button' do
+ expect(page).not_to have_content 'Request Access'
+ expect(page).not_to have_content 'Withdraw Access Request'
+ end
+end
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..aa2d906fa2e
--- /dev/null
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master manages access requests', feature: true do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ background do
+ project.request_access(user)
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'master can see access requests' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+ end
+
+ scenario 'master can grant access' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+
+ perform_enqueued_jobs { click_on 'Grant access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted"
+ end
+
+ scenario 'master can deny access' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+
+ perform_enqueued_jobs { click_on 'Deny access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied"
+ end
+
+ def expect_visible_access_request(project, user)
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "#{project.name} access requests 1"
+ expect(page).to have_content user.name
+ end
+end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
new file mode 100644
index 00000000000..79dec442818
--- /dev/null
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Member leaves project', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [user, :developer]
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user leaves project' do
+ click_link 'Leave Project'
+
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(project.users.exists?(user.id)).to be_falsey
+ end
+end
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
new file mode 100644
index 00000000000..67811b1048e
--- /dev/null
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Owner cannot leave project', feature: true do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [owner, :owner]
+ login_as(owner)
+ 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/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
new file mode 100644
index 00000000000..af420c170ef
--- /dev/null
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+feature 'Projects > Members > User requests access', feature: true do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ background do
+ project.team << [master, :master]
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user can request access to a project' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project"
+
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content 'Your request for access has been queued for review.'
+
+ expect(page).to have_content 'Withdraw Access Request'
+ expect(page).not_to have_content 'Leave Project'
+ end
+
+ scenario 'user is not listed in the project members page' do
+ click_link 'Request Access'
+
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+
+ open_project_settings_menu
+ click_link 'Members'
+
+ visit namespace_project_project_members_path(project.namespace, project)
+ page.within('.content') do
+ expect(page).not_to have_content(user.name)
+ end
+ end
+
+ scenario 'user can withdraw its request for access' do
+ click_link 'Request Access'
+
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+
+ click_link 'Withdraw Access Request'
+
+ expect(project.members.request.exists?(user_id: user)).to be_falsey
+ expect(page).to have_content 'Your access request to the project has been withdrawn.'
+ end
+
+ def open_project_settings_menu
+ find('#project-settings-button').click
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 9dd0378d165..6fa8298d489 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -70,22 +70,6 @@ feature 'Project', feature: true do
end
end
- describe 'leave project link' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
-
- before do
- login_with(user)
- project.team.add_user(user, Gitlab::Access::MASTER)
- visit namespace_project_path(project.namespace, project)
- end
-
- it 'click project-settings and find leave project' do
- find('#project-settings-button').click
- expect(page).to have_link('Leave Project')
- end
- end
-
describe 'project title' do
include WaitForAjax
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/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index c5f741709ad..f6c6687e162 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -175,6 +175,49 @@ describe "Public Project Access", feature: true do
end
end
+ describe "GET /:project_path/environments" do
+ subject { namespace_project_environments_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/:id" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environments_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_allowed_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
+ describe "GET /:project_path/environments/new" do
+ subject { new_namespace_project_environment_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for owner }
+ it { is_expected.to be_allowed_for master }
+ it { is_expected.to be_allowed_for developer }
+ it { is_expected.to be_denied_for reporter }
+ it { is_expected.to be_denied_for guest }
+ it { is_expected.to be_denied_for :user }
+ it { is_expected.to be_denied_for :external }
+ it { is_expected.to be_denied_for :visitor }
+ end
+
describe "GET /:project_path/blob" do
let(:commit) { project.repository.commit }
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 8e1833a069e..0bdb1628c74 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -103,11 +103,15 @@ describe 'Dashboard Todos', feature: true do
before do
deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true)
create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author)
+ create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author, state: :done)
login_as(user)
visit dashboard_todos_path
end
it 'shows "All done" message' do
+ within('.todos-pending-count') { expect(page).to have_content '0' }
+ expect(page).to have_content 'To do 0'
+ expect(page).to have_content 'Done 0'
expect(page).to have_content "You're all done!"
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index 366a90228b1..14613754f74 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -12,39 +12,24 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "registration" do
let(:user) { create(:user) }
- before { login_as(user) }
- describe 'when 2FA via OTP is disabled' do
- it 'allows registering a new device' do
- visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
-
- register_u2f_device
+ before do
+ login_as(user)
+ user.update_attribute(:otp_required_for_login, true)
+ end
- expect(page.body).to match('Your U2F device was registered')
- end
+ describe 'when 2FA via OTP is disabled' do
+ before { user.update_attribute(:otp_required_for_login, false) }
- it 'allows registering more than one device' do
+ it 'does not allow registering a new device' do
visit profile_account_path
-
- # First device
click_on 'Enable Two-Factor Authentication'
- register_u2f_device
- expect(page.body).to match('Your U2F device was registered')
-
- # Second device
- click_on 'Manage Two-Factor Authentication'
- register_u2f_device
- expect(page.body).to match('Your U2F device was registered')
- click_on 'Manage Two-Factor Authentication'
- expect(page.body).to match('You have 2 U2F devices registered')
+ expect(page).to have_button('Setup New U2F Device', disabled: true)
end
end
describe 'when 2FA via OTP is enabled' do
- before { user.update_attributes(otp_required_for_login: true) }
-
it 'allows registering a new device' do
visit profile_account_path
click_on 'Manage Two-Factor Authentication'
@@ -67,7 +52,6 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
click_on 'Manage Two-Factor Authentication'
register_u2f_device
expect(page.body).to match('Your U2F device was registered')
-
click_on 'Manage Two-Factor Authentication'
expect(page.body).to match('You have 2 U2F devices registered')
end
@@ -76,15 +60,16 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
it 'allows the same device to be registered for multiple users' do
# First user
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
u2f_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
logout
# Second user
- login_as(:user)
+ user = login_as(:user)
+ user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
register_u2f_device(u2f_device)
expect(page.body).to match('Your U2F device was registered')
@@ -94,7 +79,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
context "when there are form errors" do
it "doesn't register the device if there are errors" do
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
# Have the "u2f device" respond with bad data
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -109,7 +94,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
it "allows retrying registration" do
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
# Failed registration
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -133,8 +118,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
before do
# Register and logout
login_as(user)
+ user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
@u2f_device = register_u2f_device
logout
end
@@ -154,7 +140,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when 2FA via OTP is enabled" do
it "allows logging in with the U2F device" do
- user.update_attributes(otp_required_for_login: true)
+ user.update_attribute(:otp_required_for_login, true)
login_with(user)
@u2f_device.respond_to_u2f_authentication
@@ -171,8 +157,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
it "does not allow logging in with that particular device" do
# Register current user with the different U2F device
current_user = login_as(:user)
+ current_user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
register_u2f_device
logout
@@ -191,8 +178,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
it "allows logging in with that particular device" do
# Register current user with the same U2F device
current_user = login_as(:user)
+ current_user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
register_u2f_device(@u2f_device)
logout
@@ -227,8 +215,9 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
before do
login_as(user)
+ user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Enable Two-Factor Authentication'
+ click_on 'Manage Two-Factor Authentication'
register_u2f_device
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index c83824b900d..1bd354815e4 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -34,5 +34,28 @@ describe NotesFinder do
notes = NotesFinder.new.execute(project, user, params)
expect(notes).to eq([note1])
end
+
+ context 'confidential issue notes' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+
+ let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+
+ it 'returns notes if user can see the issue' do
+ expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note])
+ end
+
+ it 'raises an error if user can not see the issue' 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/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json
index 1b6008e2872..8d1b874c29b 100644
--- a/spec/fixtures/container_registry/tag_manifest.json
+++ b/spec/fixtures/container_registry/tag_manifest.json
@@ -1 +1,16 @@
-{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]}
+{
+ "schemaVersion": 2,
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "config": {
+ "mediaType": "application/octet-stream",
+ "size": 1145,
+ "digest": "sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"
+ },
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 2319870,
+ "digest": "sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"
+ }
+ ]
+}
diff --git a/spec/fixtures/container_registry/tag_manifest_1.json b/spec/fixtures/container_registry/tag_manifest_1.json
new file mode 100644
index 00000000000..d09ede5bea7
--- /dev/null
+++ b/spec/fixtures/container_registry/tag_manifest_1.json
@@ -0,0 +1,32 @@
+{
+ "schemaVersion": 1,
+ "name": "library/alpine",
+ "tag": "2.6",
+ "architecture": "amd64",
+ "fsLayers": [
+ {
+ "blobSum": "sha256:2a3ebcb7fbcc29bf40c4f62863008bb573acdea963454834d9483b3e5300c45d"
+ }
+ ],
+ "history": [
+ {
+ "v1Compatibility": "{\"id\":\"dd807873c9a21bcc82e30317c283e6601d7e19f5cf7867eec34cdd1aeb3f099e\",\"created\":\"2016-01-18T18:32:39.162138276Z\",\"container\":\"556a728876db7b0e621adc029c87c649d32520804f8f15defd67bb070dc1a88d\",\"container_config\":{\"Hostname\":\"556a728876db\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:7dee8a455bcc39013aa168d27ece9227aad155adbaacbd153d94ca60113f59fc in /\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.3\",\"config\":{\"Hostname\":\"556a728876db\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":4501436}"
+ }
+ ],
+ "signatures": [
+ {
+ "header": {
+ "jwk": {
+ "crv": "P-256",
+ "kid": "4MZL:Z5ZP:2RPA:Q3TD:QOHA:743L:EM2G:QY6Q:ZJCX:BSD7:CRYC:LQ6T",
+ "kty": "EC",
+ "x": "qmWOaxPUk7QsE5iTPdeG1e9yNE-wranvQEnWzz9FhWM",
+ "y": "WeeBpjTOYnTNrfCIxtFY5qMrJNNk9C1vc5ryxbbMD_M"
+ },
+ "alg": "ES256"
+ },
+ "signature": "0zmjTJ4m21yVwAeteLc3SsQ0miScViCDktFPR67W-ozGjjI3iBjlDjwOl6o2sds5ZI9U6bSIKOeLDinGOhHoOQ",
+ "protected": "eyJmb3JtYXRMZW5ndGgiOjEzNzIsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNi0xNVQxMDo0NDoxNFoifQ"
+ }
+ ]
+}
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index f6c1005d265..bb28866f010 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -174,51 +174,6 @@ describe ApplicationHelper do
end
end
- describe 'grouped_options_refs' do
- let(:options) { helper.grouped_options_refs }
- let(:project) { create(:project) }
-
- before do
- assign(:project, project)
-
- # Override Rails' grouped_options_for_select helper to just return the
- # first argument (`options`), since it's easier to work with than the
- # generated HTML.
- allow(helper).to receive(:grouped_options_for_select).
- and_wrap_original { |_, *args| args.first }
- end
-
- it 'includes a list of branch names' do
- expect(options[0][0]).to eq('Branches')
- expect(options[0][1]).to include('master', 'feature')
- end
-
- it 'includes a list of tag names' do
- expect(options[1][0]).to eq('Tags')
- expect(options[1][1]).to include('v1.0.0', 'v1.1.0')
- end
-
- it 'includes a specific commit ref if defined' do
- # Must be an instance variable
- ref = '2ed06dc41dbb5936af845b87d79e05bbf24c73b8'
- assign(:ref, ref)
-
- expect(options[2][0]).to eq('Commit')
- expect(options[2][1]).to eq([ref])
- end
-
- it 'sorts tags in a natural order' do
- # Stub repository.tag_names to make sure we get some valid testing data
- expect(project.repository).to receive(:tag_names).
- and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v2.0rc1¿',
- 'v1.0.9a', 'v2.0-rc1', 'v2.0rc2'])
-
- expect(options[1][1]).
- to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0rc1¿', 'v2.0-rc1', 'v1.0.10',
- 'v1.0.9', 'v1.0.9a'])
- end
- end
-
describe 'simple_sanitize' do
let(:a_tag) { '<a href="#">Foo</a>' }
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
new file mode 100644
index 00000000000..14847d0a49e
--- /dev/null
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe GitlabRoutingHelper do
+ describe 'Project URL helpers' do
+ describe '#project_members_url' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project_members_url(project)).to eq namespace_project_project_members_url(project.namespace, project) }
+ end
+
+ describe '#project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(project_member_path(project_member)).to eq namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+
+ describe '#request_access_project_members_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(request_access_project_members_path(project)).to eq request_access_namespace_project_project_members_path(project.namespace, project) }
+ end
+
+ describe '#leave_project_members_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(leave_project_members_path(project)).to eq leave_namespace_project_project_members_path(project.namespace, project) }
+ end
+
+ describe '#approve_access_request_project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(approve_access_request_project_member_path(project_member)).to eq approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+
+ describe '#resend_invite_project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(resend_invite_project_member_path(project_member)).to eq resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+ end
+
+ describe 'Group URL helpers' do
+ describe '#group_members_url' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(group_members_url(group)).to eq group_group_members_url(group) }
+ end
+
+ describe '#group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(group_member_path(group_member)).to eq group_group_member_path(group_member.source, group_member) }
+ end
+
+ describe '#request_access_group_members_path' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(request_access_group_members_path(group)).to eq request_access_group_group_members_path(group) }
+ end
+
+ describe '#leave_group_members_path' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(leave_group_members_path(group)).to eq leave_group_group_members_path(group) }
+ end
+
+ describe '#approve_access_request_group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(approve_access_request_group_member_path(group_member)).to eq approve_access_request_group_group_member_path(group_member.source, group_member) }
+ end
+
+ describe '#resend_invite_group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
+ end
+ end
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index eae61a54dfc..831ae7fb69c 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -7,10 +7,7 @@ describe IssuesHelper do
describe "url_for_project_issues" do
let(:project_url) { ext_project.external_issue_tracker.project_url }
- let(:ext_expected) do
- project_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project]) }
it "should return internal path if used internal tracker" do
@@ -56,11 +53,7 @@ describe IssuesHelper do
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
- let(:ext_expected) do
- issues_url.gsub(':id', issue.iid.to_s)
- .gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
it "should return internal path if used internal tracker" do
@@ -106,10 +99,7 @@ describe IssuesHelper do
describe 'url_for_new_issue' do
let(:issues_url) { ext_project.external_issue_tracker.new_issue_url }
- let(:ext_expected) do
- issues_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) }
it "should return internal path if used internal tracker" do
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
new file mode 100644
index 00000000000..f75fdb739f6
--- /dev/null
+++ b/spec/helpers/members_helper_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe MembersHelper do
+ describe '#action_member_permission' do
+ let(:project_member) { build(:project_member) }
+ let(:group_member) { build(:group_member) }
+
+ it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
+ it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
+ end
+
+ describe '#default_show_roles' do
+ let(:user) { double }
+ let(:member) { build(:project_member) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false)
+ allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false)
+ allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false)
+ end
+
+ context 'when the current cannot update, destroy or admin the passed member' do
+ it 'returns false' do
+ expect(helper.default_show_roles(member)).to be_falsy
+ end
+ end
+
+ context 'when the current can update the passed member' do
+ before do
+ allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+
+ context 'when the current can destroy the passed member' do
+ before do
+ allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+
+ context 'when the current can admin the passed member source' do
+ before do
+ allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+ end
+
+ describe '#remove_member_message' do
+ let(:requester) { build(:user) }
+ let(:project) { create(:project) }
+ let(:project_member) { build(:project_member, project: project) }
+ let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
+ let(:project_member_request) { project.request_access(requester) }
+ let(:group) { create(:group) }
+ let(:group_member) { build(:group_member, group: group) }
+ let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
+ let(:group_member_request) { group.request_access(requester) }
+
+ it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
+ end
+
+ describe '#remove_member_title' do
+ let(:requester) { build(:user) }
+ let(:project) { create(:project) }
+ let(:project_member) { build(:project_member, project: project) }
+ let(:project_member_request) { project.request_access(requester) }
+ let(:group) { create(:group) }
+ let(:group_member) { build(:group_member, group: group) }
+ let(:group_member_request) { group.request_access(requester) }
+
+ it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
+ it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
+ it { expect(remove_member_title(group_member)).to eq 'Remove user from group' }
+ it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
+ end
+
+ describe '#leave_confirmation_message' do
+ let(:project) { build_stubbed(:project) }
+ let(:group) { build_stubbed(:group) }
+ let(:user) { build_stubbed(:user) }
+
+ it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" }
+ it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
+ end
+end
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/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index ac5af8740dc..09e0bbfd00b 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -45,16 +45,6 @@ describe ProjectsHelper do
end
end
- describe 'user_max_access_in_project' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- before do
- project.team.add_user(user, Gitlab::Access::MASTER)
- end
-
- it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') }
- end
-
describe "readme_cache_key" do
let(:project) { create(:project) }
diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee
new file mode 100644
index 00000000000..8af39c41f2f
--- /dev/null
+++ b/spec/javascripts/application_spec.js.coffee
@@ -0,0 +1,30 @@
+#= require lib/common_utils
+
+describe 'Application', ->
+ describe 'disable buttons', ->
+ fixture.preload('application.html')
+
+ beforeEach ->
+ fixture.load('application.html')
+
+ it 'should prevent default action for disabled buttons', ->
+
+ gl.utils.preventDisabledButtons()
+
+ isClicked = false
+ $button = $ '#test-button'
+
+ $button.click -> isClicked = true
+ $button.trigger 'click'
+
+ expect(isClicked).toBe false
+
+
+ it 'should be on the same page if a disabled link clicked', ->
+
+ locationBeforeLinkClick = window.location.href
+ gl.utils.preventDisabledButtons()
+
+ $('#test-link').click()
+
+ expect(window.location.href).toBe locationBeforeLinkClick
diff --git a/spec/javascripts/fixtures/application.html.haml b/spec/javascripts/fixtures/application.html.haml
new file mode 100644
index 00000000000..3fc6114407d
--- /dev/null
+++ b/spec/javascripts/fixtures/application.html.haml
@@ -0,0 +1,2 @@
+%a#test-link.btn.disabled{:href => "/foo"} Test link
+%button#test-button.btn.disabled Test Button
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/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml
index 393c0613fd3..5ed51be689c 100644
--- a/spec/javascripts/fixtures/u2f/register.html.haml
+++ b/spec/javascripts/fixtures/u2f/register.html.haml
@@ -1 +1,2 @@
-= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' }
+- user = FactoryGirl.build(:user, :two_factor_via_otp)
+= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f', current_user: user }
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index ea27f36e9b5..71f0c1076c5 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -1,3 +1,4 @@
+#= require lib/text_utility
#= require issue
describe 'Issue', ->
@@ -38,7 +39,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeVisible()
expect($btnClose).toBeHidden()
expect($('div.status-box-closed')).toBeVisible()
@@ -50,7 +51,7 @@ describe 'reopen/close issue', ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://goesnowhere.nothing/whereami')
req.success saved: false
-
+
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
@@ -59,7 +60,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
@@ -73,7 +74,7 @@ describe 'reopen/close issue', ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://goesnowhere.nothing/whereami')
req.error()
-
+
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
@@ -82,7 +83,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
@@ -105,4 +106,4 @@ describe 'reopen/close issue', ->
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-open')).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden() \ No newline at end of file
+ expect($('div.status-box-closed')).toBeHidden()
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..1ee31a603e4
--- /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).exactly(4).times.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..5b63c946114 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
@@ -131,6 +134,12 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
+
+ it 'ignores out-of-bounds issue IDs on the referenced project' do
+ exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
end
context 'cross-project URL reference' do
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 3185e41fe5c..805acf1c8b3 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -38,6 +38,12 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
+ it 'ignores out-of-bounds merge request IDs on the referenced project' do
+ exp = act = "Merge !#{Gitlab::Database::MAX_INT_VALUE + 1}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
it 'includes a title attribute' do
doc = reference_filter("Merge #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index 697d10bbf70..f181125156b 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -69,6 +69,18 @@ describe Banzai::Filter::RedactorFilter, lib: true do
expect(doc.css('a').length).to eq 0
end
+ it 'removes references for project members with guest role' do
+ member = create(:user)
+ project = create(:empty_project, :public)
+ project.team << [member, :guest]
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
+ doc = filter(link, current_user: member)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
it 'allows references for author' do
author = create(:user)
project = create(:empty_project, :public)
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 0e6685f0ffb..b9e4a4eaf0e 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -132,11 +132,8 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
path = 'files/images/한글.png'
escaped = Addressable::URI.escape(path)
- # Stub these methods so the file doesn't actually need to be in the repo
- allow_any_instance_of(described_class).
- to receive(:file_exists?).and_return(true)
- allow_any_instance_of(described_class).
- to receive(:image?).with(path).and_return(true)
+ # Stub this method so the file doesn't actually need to be in the repo
+ allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw)
doc = filter(image(escaped))
expect(doc.at_css('img')['src']).to match '/raw/'
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/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
new file mode 100644
index 00000000000..92d88c4172c
--- /dev/null
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Banzai::Filter::WikiLinkFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
+ let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
+ let(:user) { double }
+ let(:wiki) { ProjectWiki.new(project, user) }
+
+ it "doesn't rewrite absolute links" do
+ filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0]
+ expect(filtered_link.attribute('href').value).to eq('http://example.com:8000/')
+ end
+
+ describe "invalid links" do
+ invalid_links = ["http://:8080", "http://", "http://:8080/path"]
+
+ invalid_links.each do |invalid_link|
+ it "doesn't rewrite invalid invalid_links like #{invalid_link}" do
+ filtered_link = filter("<a href='#{invalid_link}'>Link</a>", project_wiki: wiki).children[0]
+ expect(filtered_link.attribute('href').value).to eq(invalid_link)
+ end
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 304290d6608..d562d8b25ea 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -26,7 +26,8 @@ module Ci
tag_list: [],
options: {},
allow_failure: false,
- when: "on_success"
+ when: "on_success",
+ environment: nil,
})
end
@@ -156,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
@@ -283,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
@@ -301,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
{
@@ -387,7 +445,8 @@ module Ci
services: ["mysql"]
},
allow_failure: false,
- when: "on_success"
+ when: "on_success",
+ environment: nil,
})
end
@@ -415,7 +474,8 @@ module Ci
services: ["postgresql"]
},
allow_failure: false,
- when: "on_success"
+ when: "on_success",
+ environment: nil,
})
end
end
@@ -462,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
@@ -573,7 +655,12 @@ module Ci
services: ["mysql"],
before_script: ["pwd"],
rspec: {
- artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
+ artifacts: {
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ name: "custom_name",
+ expire_in: "7d"
+ },
script: "rspec"
}
})
@@ -595,11 +682,13 @@ module Ci
artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"],
- untracked: true
+ untracked: true,
+ expire_in: "7d"
}
},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
end
@@ -621,6 +710,51 @@ module Ci
end
end
+ describe '#environment' do
+ let(:config) do
+ {
+ deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
+ }
+ end
+
+ let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
+ let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
+
+ context 'when a production environment is specified' do
+ let(:environment) { 'production' }
+
+ it 'does return production' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment)
+ end
+ end
+
+ context 'when no environment is specified' do
+ let(:environment) { nil }
+
+ it 'does return nil environment' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to be_nil
+ end
+ end
+
+ context 'is not a string' do
+ let(:environment) { 1 }
+
+ it 'raises error' do
+ expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ end
+ end
+
+ context 'is not a valid string' do
+ let(:environment) { 'production staging' }
+
+ it 'raises error' do
+ expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ end
+ end
+ end
+
describe "Dependencies" do
let(:config) do
{
@@ -682,7 +816,8 @@ module Ci
tag_list: [],
options: {},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
end
end
@@ -727,7 +862,8 @@ module Ci
tag_list: [],
options: {},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
expect(subject.second).to eq({
except: nil,
@@ -739,7 +875,8 @@ module Ci
tag_list: [],
options: {},
when: "on_success",
- allow_failure: false
+ allow_failure: false,
+ environment: nil,
})
end
end
@@ -992,6 +1129,20 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always")
end
+ it "returns errors if job artifacts:expire_in is not an a string" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a valid duration" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ end
+
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
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/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index 858cb0bb134..c7324c2bf77 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -17,46 +17,85 @@ describe ContainerRegistry::Tag do
end
context 'manifest processing' do
- before do
- stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
- with(headers: headers).
- to_return(
- status: 200,
- body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'),
- headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
- end
+ context 'schema v1' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
+ with(headers: headers).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest_1.json'),
+ headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v1+prettyjws' })
+ end
- context '#layers' do
- subject { tag.layers }
+ context '#layers' do
+ subject { tag.layers }
- it { expect(subject.length).to eq(1) }
- end
+ it { expect(subject.length).to eq(1) }
+ end
+
+ context '#total_size' do
+ subject { tag.total_size }
- context '#total_size' do
- subject { tag.total_size }
+ it { is_expected.to be_nil }
+ end
- it { is_expected.to eq(2319870) }
+ context 'config processing' do
+ context '#config' do
+ subject { tag.config }
+
+ it { is_expected.to be_nil }
+ end
+
+ context '#created_at' do
+ subject { tag.created_at }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
- context 'config processing' do
+ context 'schema v2' do
before do
- stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
- with(headers: { 'Accept' => 'application/octet-stream' }).
+ stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
+ with(headers: headers).
to_return(
status: 200,
- body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'),
+ headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
end
- context '#config' do
- subject { tag.config }
+ context '#layers' do
+ subject { tag.layers }
- it { is_expected.not_to be_nil }
+ it { expect(subject.length).to eq(1) }
end
- context '#created_at' do
- subject { tag.created_at }
+ context '#total_size' do
+ subject { tag.total_size }
+
+ it { is_expected.to eq(2319870) }
+ end
+
+ context 'config processing' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ with(headers: { 'Accept' => 'application/octet-stream' }).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
+ end
+
+ context '#config' do
+ subject { tag.config }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context '#created_at' do
+ subject { tag.created_at }
- it { is_expected.not_to be_nil }
+ it { is_expected.not_to be_nil }
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
new file mode 100644
index 00000000000..47c68f96dc8
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Configurable do
+ let(:node) { Class.new }
+
+ before do
+ node.include(described_class)
+ end
+
+ describe 'allowed nodes' do
+ before do
+ node.class_eval do
+ allow_node :object, Object, description: 'test object'
+ end
+ end
+
+ describe '#allowed_nodes' do
+ it 'has valid allowed nodes' do
+ expect(node.allowed_nodes).to include :object
+ end
+
+ it 'creates a node factory' do
+ expect(node.allowed_nodes[:object])
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Factory
+ end
+
+ it 'returns a duplicated factory object' do
+ first_factory = node.allowed_nodes[:object]
+ second_factory = node.allowed_nodes[:object]
+
+ expect(first_factory).not_to be_equal(second_factory)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
new file mode 100644
index 00000000000..d681aa32456
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Factory do
+ describe '#create!' do
+ let(:factory) { described_class.new(entry_class) }
+ let(:entry_class) { Gitlab::Ci::Config::Node::Script }
+
+ context 'when value setting value' do
+ it 'creates entry with valid value' do
+ entry = factory
+ .with(value: ['ls', 'pwd'])
+ .create!
+
+ expect(entry.value).to eq "ls\npwd"
+ end
+
+ context 'when setting description' do
+ it 'creates entry with description' do
+ entry = factory
+ .with(value: ['ls', 'pwd'])
+ .with(description: 'test description')
+ .create!
+
+ expect(entry.value).to eq "ls\npwd"
+ expect(entry.description).to eq 'test description'
+ end
+ end
+ end
+
+ context 'when not setting value' do
+ it 'raises error' do
+ expect { factory.create! }.to raise_error(
+ Gitlab::Ci::Config::Node::Factory::InvalidFactory
+ )
+ end
+ end
+
+ context 'when creating a null entry' do
+ it 'creates a null entry' do
+ entry = factory
+ .with(value: nil)
+ .nullify!
+ .create!
+
+ expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Null
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
new file mode 100644
index 00000000000..b1972172435
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Global do
+ let(:global) { described_class.new(hash) }
+
+ describe '#allowed_nodes' do
+ it 'can contain global config keys' do
+ expect(global.allowed_nodes).to include :before_script
+ end
+
+ it 'returns a hash' do
+ expect(global.allowed_nodes).to be_a Hash
+ end
+ end
+
+ context 'when hash is valid' do
+ let(:hash) do
+ { before_script: ['ls', 'pwd'] }
+ end
+
+ describe '#process!' do
+ before { global.process! }
+
+ it 'creates nodes hash' do
+ expect(global.nodes).to be_an Array
+ end
+
+ it 'creates node object for each entry' do
+ expect(global.nodes.count).to eq 1
+ end
+
+ it 'creates node object using valid class' do
+ expect(global.nodes.first)
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Script
+ end
+
+ it 'sets correct description for nodes' do
+ expect(global.nodes.first.description)
+ .to eq 'Script that will be executed before each job.'
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
+ end
+
+ describe '#before_script' do
+ context 'when processed' do
+ before { global.process! }
+
+ it 'returns correct script' do
+ expect(global.before_script).to eq "ls\npwd"
+ end
+ end
+
+ context 'when not processed' do
+ it 'returns nil' do
+ expect(global.before_script).to be nil
+ end
+ end
+ end
+ end
+
+ context 'when hash is not valid' do
+ before { global.process! }
+
+ let(:hash) do
+ { before_script: 'ls' }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'reports errors from child nodes' do
+ expect(global.errors)
+ .to include 'before_script should be an array of strings'
+ end
+ end
+
+ describe '#before_script' do
+ it 'raises error' do
+ expect { global.before_script }.to raise_error(
+ Gitlab::Ci::Config::Node::Entry::InvalidError
+ )
+ end
+ end
+ end
+
+ context 'when value is not a hash' do
+ let(:hash) { [] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb
new file mode 100644
index 00000000000..36101c62462
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/null_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Null do
+ let(:entry) { described_class.new(nil) }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(entry).to be_leaf
+ end
+ end
+
+ describe '#any_method' do
+ it 'responds with nil' do
+ expect(entry.any_method).to be nil
+ end
+ end
+
+ describe '#value' do
+ it 'returns nil' do
+ expect(entry.value).to be nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
new file mode 100644
index 00000000000..e4d6481f8a5
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Script do
+ let(:entry) { described_class.new(value) }
+
+ describe '#validate!' do
+ before { entry.validate! }
+
+ context 'when entry value is correct' do
+ let(:value) { ['ls', 'pwd'] }
+
+ describe '#value' do
+ it 'returns concatenated command' do
+ expect(entry.value).to eq "ls\npwd"
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:value) { 'ls' }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include /should be an array of strings/
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 4d46abe520f..3871d939feb 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -29,17 +29,43 @@ describe Gitlab::Ci::Config do
expect(config.to_hash).to eq hash
end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+
+ it 'has no errors' do
+ expect(config.errors).to be_empty
+ end
+ end
end
context 'when config is invalid' do
- let(:yml) { '// invalid' }
-
- describe '.new' do
- it 'raises error' do
- expect { config }.to raise_error(
- Gitlab::Ci::Config::Loader::FormatError,
- /Invalid configuration format/
- )
+ context 'when yml is incorrect' do
+ let(:yml) { '// invalid' }
+
+ describe '.new' do
+ it 'raises error' do
+ expect { config }.to raise_error(
+ Gitlab::Ci::Config::Loader::FormatError,
+ /Invalid configuration format/
+ )
+ end
+ end
+ end
+
+ context 'when config logic is incorrect' do
+ let(:yml) { 'before_script: "ls"' }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'has errors' do
+ expect(config.errors).not_to be_empty
+ end
end
end
end
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/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
index 88814bc474d..659facd6c19 100644
--- a/spec/lib/gitlab/lfs/lfs_router_spec.rb
+++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb
@@ -17,12 +17,15 @@ describe Gitlab::Lfs::Router, lib: true do
}
end
- let(:lfs_router_auth) { new_lfs_router(project, user) }
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
- let(:lfs_router_public_auth) { new_lfs_router(public_project, user) }
- let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) }
- let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) }
- let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) }
+ let(:lfs_router_auth) { new_lfs_router(project, user: user) }
+ let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) }
+ let(:lfs_router_noauth) { new_lfs_router(project) }
+ let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) }
+ let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) }
+ let(:lfs_router_public_noauth) { new_lfs_router(public_project) }
+ let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) }
+ let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) }
+ let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) }
let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
let(:sample_size) { 499013 }
@@ -80,6 +83,7 @@ describe Gitlab::Lfs::Router, lib: true do
context 'with required headers' do
before do
+ project.lfs_objects << lfs_object
env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
end
@@ -91,7 +95,6 @@ describe Gitlab::Lfs::Router, lib: true do
context 'when user has project access' do
before do
- project.lfs_objects << lfs_object
project.team << [user, :master]
end
@@ -104,6 +107,17 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
end
end
+
+ context 'when CI is authorized' do
+ it "responds with status 200" do
+ expect(lfs_router_ci_auth.try_call.first).to eq(200)
+ end
+
+ it "responds with the file location" do
+ expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
+ expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
end
context 'without required headers' do
@@ -134,143 +148,145 @@ describe Gitlab::Lfs::Router, lib: true do
end
describe 'download' do
- describe 'when user is authenticated' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
- describe 'when user has download access' do
+ shared_examples 'an authorized requests' do
+ context 'when downloading an lfs object that is assigned to our project' do
before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :reporter]
+ project.lfs_objects << lfs_object
end
- context 'when downloading an lfs object that is assigned to our project' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and href to download' do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it 'responds with status 200 and href to download' do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => @auth }
- }
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => auth }
}
- }])
- end
+ }
+ }])
end
+ end
- context 'when downloading an lfs object that is assigned to other project' do
- before do
- public_project.lfs_objects << lfs_object
- end
+ context 'when downloading an lfs object that is assigned to other project' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
- it 'responds with status 200 and error message' do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it 'responds with status 200 and error message' do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
end
+ end
- context 'when downloading a lfs object that does not exist' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ context 'when downloading a lfs object that does not exist' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
- it "responds with status 200 and error message" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it "responds with status 200 and error message" do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
end
+ end
- context 'when downloading one new and one existing lfs object' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- project.lfs_objects << lfs_object
- end
+ context 'when downloading one new and one existing lfs object' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ project.lfs_objects << lfs_object
+ end
- it "responds with status 200 with upload hypermedia link for the new object" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it "responds with status 200 with upload hypermedia link for the new object" do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- },
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => @auth }
- }
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => auth }
}
- }])
- end
+ }
+ }])
end
end
+ end
+
+ context 'when user is authenticated' do
+ let(:auth) { authorize(user) }
+
+ before do
+ env["HTTP_AUTHORIZATION"] = auth
+ project.team << [user, role]
+ end
+
+ it_behaves_like 'an authorized requests' do
+ let(:role) { :reporter }
+ let(:router) { lfs_router_auth }
+ end
context 'when user does is not member of the project' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :guest]
- end
+ let(:role) { :guest }
it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403)
@@ -278,11 +294,7 @@ describe Gitlab::Lfs::Router, lib: true do
end
context 'when user does not have download access' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :guest]
- end
+ let(:role) { :guest }
it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403)
@@ -290,18 +302,19 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- context 'when user is not authenticated' do
+ context 'when CI is authorized' do
+ let(:auth) { 'gitlab-ci-token:password' }
+
before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }],
+ env["HTTP_AUTHORIZATION"] = auth
+ end
- }.to_json
- env['rack.input'] = StringIO.new(body)
+ it_behaves_like 'an authorized requests' do
+ let(:router) { lfs_router_ci_auth }
end
+ end
+ context 'when user is not authenticated' do
describe 'is accessing public project' do
before do
public_project.lfs_objects << lfs_object
@@ -338,17 +351,17 @@ describe Gitlab::Lfs::Router, lib: true do
end
describe 'upload' do
- describe 'when user is authenticated' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ before do
+ body = { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+ describe 'when request is authenticated' do
describe 'when user has project push access' do
before do
@auth = authorize(user)
@@ -440,15 +453,15 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call.first).to eq(403)
end
end
- end
- context 'when user is not authenticated' do
- before do
- env['rack.input'] = StringIO.new(
- { 'objects' => [], 'operation' => 'upload' }.to_json
- )
+ context 'when CI is authorized' do
+ it 'responds with 401' do
+ expect(lfs_router_ci_auth.try_call.first).to eq(401)
+ end
end
+ end
+ context 'when user is not authenticated' do
context 'when user has push access' do
before do
project.team << [user, :master]
@@ -465,6 +478,18 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
end
+
+ context 'when CI is authorized' do
+ let(:auth) { 'gitlab-ci-token:password' }
+
+ before do
+ env["HTTP_AUTHORIZATION"] = auth
+ end
+
+ it "responds with status 403" do
+ expect(lfs_router_public_ci_auth.try_call.first).to eq(401)
+ end
+ end
end
describe 'unsupported' do
@@ -490,13 +515,68 @@ describe Gitlab::Lfs::Router, lib: true do
env['REQUEST_METHOD'] = 'PUT'
end
- describe 'to one project' do
- describe 'when user has push access to the project' do
+ shared_examples 'unauthorized' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
- project.team << [user, :master]
+ header_for_upload_authorize(router.project)
+ end
+
+ it 'responds with status 401' do
+ expect(router.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(router.project)
+ end
+
+ it 'responds with status 401' do
+ expect(router.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(router.try_call).to eq(nil)
+ end
+ end
+ end
+
+ shared_examples 'forbidden' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(router.project)
+ end
+
+ it 'responds with 403' do
+ expect(router.try_call.first).to eq(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(router.project)
+ end
+
+ it 'responds with 403' do
+ expect(router.try_call.first).to eq(403)
end
+ end
+ end
+
+ describe 'to one project' do
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :developer]
+ end
- describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(project)
@@ -524,100 +604,35 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- describe 'when user is unauthenticated' do
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+ describe 'and user does not have push access' do
+ let(:router) { lfs_router_auth }
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent with a malformed headers' do
- before do
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
- env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
- end
-
- it 'does not recognize it as a valid lfs command' do
- expect(lfs_router_noauth.try_call).to eq(nil)
- end
- end
+ it_behaves_like 'forbidden'
end
end
- describe 'and user does not have push access' do
- describe 'when user is authenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
- end
+ context 'when CI is authenticated' do
+ let(:router) { lfs_router_ci_auth }
- describe 'when user is unauthenticated' do
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
-
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
+ it_behaves_like 'unauthorized'
+ end
- it 'responds with 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
+ context 'for unauthenticated' do
+ let(:router) { new_lfs_router(project) }
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
- end
+ it_behaves_like 'unauthorized'
end
end
- describe "to a forked project" do
+ describe 'to a forked project' do
let(:forked_project) { fork_project(public_project, user) }
- describe 'when user has push access to the project' do
- before do
- forked_project.team << [user_two, :master]
- end
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
+ before do
+ forked_project.team << [user_two, :developer]
+ end
- describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(forked_project)
@@ -645,78 +660,28 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- describe 'when user is unauthenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
+ describe 'and user does not have push access' do
+ let(:router) { lfs_router_forked_auth }
- it 'responds with status 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
+ it_behaves_like 'forbidden'
end
end
- describe 'and user does not have push access' do
- describe 'when user is authenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
+ context 'when CI is authenticated' do
+ let(:router) { lfs_router_forked_ci_auth }
- it 'responds with 403' do
- expect(lfs_router_forked_auth.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_forked_auth.try_call.first).to eq(403)
- end
- end
- end
-
- describe 'when user is unauthenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
+ it_behaves_like 'unauthorized'
+ end
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
+ context 'for unauthenticated' do
+ let(:router) { lfs_router_forked_noauth }
- it 'responds with 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
- end
+ it_behaves_like 'unauthorized'
end
describe 'and second project not related to fork or a source project' do
let(:second_project) { create(:project) }
- let(:lfs_router_second_project) { new_lfs_router(second_project, user) }
+ let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) }
before do
public_project.lfs_objects << lfs_object
@@ -745,8 +710,8 @@ describe Gitlab::Lfs::Router, lib: true do
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
- def new_lfs_router(project, user)
- Gitlab::Lfs::Router.new(project, user, request)
+ def new_lfs_router(project, user: nil, ci: false)
+ Gitlab::Lfs::Router.new(project, user, ci, request)
end
def header_for_upload_authorize(project)
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 220e86924a2..8809b7e3f12 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -9,9 +9,31 @@ describe Gitlab::Metrics::Instrumentation do
text
end
+ class << self
+ def buzz(text = 'buzz')
+ text
+ end
+ private :buzz
+
+ def flaky(text = 'flaky')
+ text
+ end
+ protected :flaky
+ end
+
def bar(text = 'bar')
text
end
+
+ def wadus(text = 'wadus')
+ text
+ end
+ private :wadus
+
+ def chaf(text = 'chaf')
+ text
+ end
+ protected :chaf
end
allow(@dummy).to receive(:name).and_return('Dummy')
@@ -56,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, an_instance_of(Hash),
- method: 'Dummy.foo')
+ expect(transaction).to receive(:measure_method).
+ with('Dummy.foo')
@dummy.foo
end
@@ -136,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, an_instance_of(Hash),
- method: 'Dummy#bar')
+ expect(transaction).to receive(:measure_method).
+ with('Dummy#bar')
@dummy.new.bar
end
@@ -208,6 +228,21 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_methods(@dummy)
expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:foo).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all protected class methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:flaky).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all private instance methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:buzz).source_location.first).to match(/instrumentation\.rb/)
end
it 'only instruments methods directly defined in the module' do
@@ -241,6 +276,21 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_instance_methods(@dummy)
expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:bar).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all protected instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:chaf).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all private instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:wadus).source_location.first).to match(/instrumentation\.rb/)
end
it 'only instruments methods directly defined in the module' do
@@ -253,7 +303,7 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_instance_methods(@dummy)
- expect(@dummy.method_defined?(:_original_kittens)).to eq(false)
+ expect(@dummy.new.method(:kittens).source_location.first).not_to match(/instrumentation\.rb/)
end
it 'can take a block to determine if a method should be instrumented' do
@@ -261,7 +311,7 @@ describe Gitlab::Metrics::Instrumentation do
false
end
- expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ expect(@dummy.new.method(:bar).source_location.first).not_to match(/instrumentation\.rb/)
end
end
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 b99be4e1060..f264ed64029 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -31,6 +31,20 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
+
+ it 'tags a transaction with the method andpath of the route in the grape endpoint' do
+ route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+
+ allow(app).to receive(:call).with(env)
+
+ expect(middleware).to receive(:tag_endpoint).
+ with(an_instance_of(Gitlab::Metrics::Transaction), env)
+
+ middleware.call(env)
+ end
end
describe '#transaction_from_env' do
@@ -44,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
@@ -60,4 +90,19 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction.action).to eq('TestController#show')
end
end
+
+ describe '#tag_endpoint' do
+ let(:transaction) { middleware.transaction_from_env(env) }
+
+ it 'tags a transaction with the method and path of the route in the grape endpount' do
+ route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+
+ middleware.tag_endpoint(transaction, env)
+
+ expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb
index 59db127674a..1ab923b58cf 100644
--- a/spec/lib/gitlab/metrics/sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/sampler_spec.rb
@@ -72,14 +72,25 @@ describe Gitlab::Metrics::Sampler do
end
end
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler).to receive(:add_metric).
- with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
- at_least(:once).
- and_call_original
+ if Gitlab::Metrics.mri?
+ describe '#sample_objects' do
+ it 'adds a metric containing the amount of allocated objects' do
+ expect(sampler).to receive(:add_metric).
+ with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
+ at_least(:once).
+ and_call_original
+
+ sampler.sample_objects
+ end
- sampler.sample_objects
+ it 'ignores classes without a name' do
+ expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
+
+ expect(sampler).not_to receive(:add_metric).
+ with('object_counts', an_instance_of(Hash), type: nil)
+
+ sampler.sample_objects
+ end
end
end
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/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index db0ff95b4f5..270b89972d7 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -43,6 +43,18 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ results = described_class.new(member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 1
+ end
+
it 'should list project confidential issues for author' do
results = described_class.new(author, project, query)
issues = results.objects('issues')
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/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index f4afe597e8d..1bb444bf34f 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -86,6 +86,22 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 1
end
+ it 'should not list confidential issues for project members with guest role' do
+ project_1.team << [member, :guest]
+ project_2.team << [member, :guest]
+
+ results = described_class.new(member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 1
+ end
+
it 'should list confidential issues for author' do
results = described_class.new(author, limit_projects, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_spec.rb
index 72baa516cc4..bc0ec9325cc 100644
--- a/spec/lib/gitlab/gitignore_spec.rb
+++ b/spec/lib/gitlab/template/gitignore_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-describe Gitlab::Gitignore do
- subject { Gitlab::Gitignore }
+describe Gitlab::Template::Gitignore do
+ subject { described_class }
describe '.all' do
it 'strips the gitignore suffix' do
@@ -24,7 +24,7 @@ describe Gitlab::Gitignore do
it 'returns the Gitignore object of a valid file' do
ruby = subject.find('Ruby')
- expect(ruby).to be_a Gitlab::Gitignore
+ expect(ruby).to be_a Gitlab::Template::Gitignore
expect(ruby.name).to eq('Ruby')
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 818825b1477..ae55a01ebea 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -400,26 +400,169 @@ describe Notify do
end
end
+ describe 'project access requested' do
+ context 'for a project in a user namespace' do
+ let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('project', project_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ to_emails = subject.header[:to].addrs
+ expect(to_emails.size).to eq(1)
+ expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email)
+
+ is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+ is_expected.to have_body_text /#{project_member.human_access}/
+ end
+ end
+
+ context 'for a project in a group' do
+ let(:group_owner) { create(:user) }
+ let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
+ let(:project) { create(:project, namespace: group) }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('project', project_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ to_emails = subject.header[:to].addrs
+ expect(to_emails.size).to eq(1)
+ expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email)
+
+ is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+ is_expected.to have_body_text /#{project_member.human_access}/
+ end
+ end
+ end
+
+ describe 'project access denied' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_denied_email('project', project.id, user.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ end
+ end
+
describe 'project access changed' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
- subject { Notify.project_access_granted_email(project_member.id) }
+ subject { Notify.member_access_granted_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
- it 'has the correct subject' do
- is_expected.to have_subject /Access to project was granted/
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.human_access}/
end
+ end
- it 'contains name of project' do
- is_expected.to have_body_text /#{project.name}/
- end
+ def invite_to_project(project:, email:, inviter:)
+ ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
- it 'contains new user role' do
+ project.project_members.invite.last
+ end
+
+ describe 'project invitation' do
+ let(:project) { create(:project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
+
+ subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
is_expected.to have_body_text /#{project_member.human_access}/
+ is_expected.to have_body_text /#{project_member.invite_token}/
+ end
+ end
+
+ describe 'project invitation accepted' do
+ let(:project) { create(:project) }
+ let(:invited_user) { create(:user) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) do
+ invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee.accept_invite!(invited_user)
+ invitee
+ end
+
+ subject { Notify.member_invite_accepted_email('project', project_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation accepted'
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.invite_email}/
+ is_expected.to have_body_text /#{invited_user.name}/
+ end
+ end
+
+ describe 'project invitation declined' do
+ let(:project) { create(:project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) do
+ invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee.decline_invite!
+ invitee
+ end
+
+ subject { Notify.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation declined'
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.invite_email}/
end
end
@@ -535,27 +678,139 @@ describe Notify do
end
end
- describe 'group access changed' do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
- let(:membership) { create(:group_member, group: group, user: user) }
+ context 'for a group' do
+ describe 'group access requested' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) do
+ group.request_access(user)
+ group.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('group', group_member.id) }
- subject { Notify.group_access_granted_email(membership.id) }
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Request to join the #{group.name} group"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group_group_members_url(group)}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ end
+ end
- it 'has the correct subject' do
- is_expected.to have_subject /Access to group was granted/
+ describe 'group access denied' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) do
+ group.request_access(user)
+ group.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_denied_email('group', group.id, user.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{group.name} group was denied"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ end
end
- it 'contains name of project' do
- is_expected.to have_body_text /#{group.name}/
+ describe 'group access changed' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) { create(:group_member, group: group, user: user) }
+
+ subject { Notify.member_access_granted_email('group', group_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{group.name} group was granted"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ end
+ end
+
+ def invite_to_group(group:, email:, inviter:)
+ GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+
+ group.group_members.invite.last
+ end
+
+ describe 'group invitation' do
+ let(:group) { create(:group) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
+
+ subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Invitation to join the #{group.name} group"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ is_expected.to have_body_text /#{group_member.invite_token}/
+ end
end
- it 'contains new user role' do
- is_expected.to have_body_text /#{membership.human_access}/
+ describe 'group invitation accepted' do
+ let(:group) { create(:group) }
+ let(:invited_user) { create(:user) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) do
+ invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee.accept_invite!(invited_user)
+ invitee
+ end
+
+ subject { Notify.member_invite_accepted_email('group', group_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation accepted'
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.invite_email}/
+ is_expected.to have_body_text /#{invited_user.name}/
+ end
+ end
+
+ describe 'group invitation declined' do
+ let(:group) { create(:group) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) do
+ invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee.decline_invite!
+ invitee
+ end
+
+ subject { Notify.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation declined'
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.invite_email}/
+ end
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/build_spec.rb b/spec/models/build_spec.rb
index 2beb6cc598d..8154001cf46 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -2,7 +2,12 @@ require 'spec_helper'
describe Ci::Build, models: true do
let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id)
+ end
+
let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to validate_presence_of :ref }
@@ -36,32 +41,44 @@ describe Ci::Build, models: true do
subject { build.ignored? }
context 'if build is not allowed to fail' do
- before { build.allow_failure = false }
+ before do
+ build.allow_failure = false
+ end
context 'and build.status is success' do
- before { build.status = 'success' }
+ before do
+ build.status = 'success'
+ end
it { is_expected.to be_falsey }
end
context 'and build.status is failed' do
- before { build.status = 'failed' }
+ before do
+ build.status = 'failed'
+ end
it { is_expected.to be_falsey }
end
end
context 'if build is allowed to fail' do
- before { build.allow_failure = true }
+ before do
+ build.allow_failure = true
+ end
context 'and build.status is success' do
- before { build.status = 'success' }
+ before do
+ build.status = 'success'
+ end
it { is_expected.to be_falsey }
end
context 'and build.status is failed' do
- before { build.status = 'failed' }
+ before do
+ build.status = 'failed'
+ end
it { is_expected.to be_truthy }
end
@@ -75,7 +92,9 @@ describe Ci::Build, models: true do
context 'if build.trace contains text' do
let(:text) { 'example output' }
- before { build.trace = text }
+ before do
+ build.trace = text
+ end
it { is_expected.to include(text) }
it { expect(subject.length).to be >= text.length }
@@ -188,7 +207,9 @@ describe Ci::Build, models: true do
]
end
- before { build.update_attributes(stage: 'stage') }
+ before do
+ build.update_attributes(stage: 'stage')
+ end
it { is_expected.to eq(predefined_variables + yaml_variables) }
@@ -199,7 +220,9 @@ describe Ci::Build, models: true do
]
end
- before { build.update_attributes(tag: true) }
+ before do
+ build.update_attributes(tag: true)
+ end
it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) }
end
@@ -257,57 +280,6 @@ describe Ci::Build, models: true do
end
end
- describe '#can_be_served?' do
- let(:runner) { create(:ci_runner) }
-
- before { build.project.runners << runner }
-
- context 'when runner does not have tags' do
- it 'can handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it 'cannot handle build with tags' do
- build.tag_list = ['aa']
- expect(build.can_be_served?(runner)).to be_falsey
- end
- end
-
- context 'when runner has tags' do
- before { runner.tag_list = ['bb', 'cc'] }
-
- shared_examples 'tagged build picker' do
- it 'can handle build with matching tags' do
- build.tag_list = ['bb']
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it 'cannot handle build without matching tags' do
- build.tag_list = ['aa']
- expect(build.can_be_served?(runner)).to be_falsey
- end
- end
-
- context 'when runner can pick untagged jobs' do
- it 'can handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it_behaves_like 'tagged build picker'
- end
-
- context 'when runner can not pick untagged jobs' do
- before { runner.run_untagged = false }
-
- it 'can not handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_falsey
- end
-
- it_behaves_like 'tagged build picker'
- end
- end
- end
-
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
@@ -348,7 +320,7 @@ describe Ci::Build, models: true do
end
it 'that cannot handle build' do
- expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false)
+ expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
is_expected.to be_falsey
end
@@ -360,7 +332,9 @@ describe Ci::Build, models: true do
%w(pending).each do |state|
context "if commit_status.status is #{state}" do
- before { build.status = state }
+ before do
+ build.status = state
+ end
it { is_expected.to be_truthy }
@@ -379,7 +353,9 @@ describe Ci::Build, models: true do
%w(success failed canceled running).each do |state|
context "if commit_status.status is #{state}" do
- before { build.status = state }
+ before do
+ build.status = state
+ end
it { is_expected.to be_falsey }
end
@@ -390,16 +366,44 @@ describe Ci::Build, models: true do
subject { build.artifacts? }
context 'artifacts archive does not exist' do
- before { build.update_attributes(artifacts_file: nil) }
+ before do
+ build.update_attributes(artifacts_file: nil)
+ end
+
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
let(:build) { create(:ci_build, :artifacts) }
it { is_expected.to be_truthy }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+ it { is_expected.to be_truthy }
+ end
end
end
+ describe '#artifacts_expired?' do
+ subject { build.artifacts_expired? }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
describe '#artifacts_metadata?' do
subject { build.artifacts_metadata? }
@@ -412,7 +416,6 @@ describe Ci::Build, models: true do
it { is_expected.to be_truthy }
end
end
-
describe '#repo_url' do
let(:build) { create(:ci_build) }
let(:project) { build.project }
@@ -427,6 +430,50 @@ describe Ci::Build, models: true do
it { is_expected.to include(project.web_url[7..-1]) }
end
+ describe '#artifacts_expire_in' do
+ subject { build.artifacts_expire_in }
+ it { is_expected.to be_nil }
+
+ context 'when artifacts_expire_at is specified' do
+ let(:expire_at) { Time.now + 7.days }
+
+ before { build.artifacts_expire_at = expire_at }
+
+ it { is_expected.to be_within(5).of(expire_at - Time.now) }
+ end
+ end
+
+ describe '#artifacts_expire_in=' do
+ subject { build.artifacts_expire_in }
+
+ it 'when assigning valid duration' do
+ build.artifacts_expire_in = '7 days'
+
+ is_expected.to be_within(10).of(7.days.to_i)
+ end
+
+ it 'when assigning invalid duration' do
+ expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
+ is_expected.to be_nil
+ end
+
+ it 'when resseting value' do
+ build.artifacts_expire_in = nil
+
+ is_expected.to be_nil
+ end
+ end
+
+ describe '#keep_artifacts!' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'to reset expire_at' do
+ build.keep_artifacts!
+
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+
describe '#depends_on_builds' do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
@@ -555,7 +602,9 @@ describe Ci::Build, models: true do
let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
describe '#erase' do
- before { build.erase(erased_by: user) }
+ before do
+ build.erase(erased_by: user)
+ end
context 'erased by user' do
let!(:user) { create(:user, username: 'eraser') }
@@ -592,7 +641,9 @@ describe Ci::Build, models: true do
end
context 'build has been erased' do
- before { build.erase }
+ before do
+ build.erase
+ end
it { is_expected.to be true }
end
@@ -600,7 +651,9 @@ describe Ci::Build, models: true do
context 'metadata and build trace are not available' do
let!(:build) { create(:ci_build, :success, :artifacts) }
- before { build.remove_artifacts_metadata! }
+ before do
+ build.remove_artifacts_metadata!
+ end
describe '#erase' do
it 'should not raise error' do
@@ -610,4 +663,10 @@ describe Ci::Build, models: true do
end
end
end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(build.commit).to eq project.commit
+ end
+ 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/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 5d04d8ffcff..ef65eb99328 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -20,34 +20,36 @@ describe Ci::Runner, models: true do
end
describe '#display_name' do
- it 'should return the description if it has a value' do
+ it 'returns the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
end
- it 'should return the token if it does not have a description' do
+ it 'returns the token if it does not have a description' do
runner = FactoryGirl.create(:ci_runner)
expect(runner.display_name).to eq runner.description
end
- it 'should return the token if the description is an empty string' do
+ it 'returns the token if the description is an empty string' do
runner = FactoryGirl.build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token
end
end
- describe :assign_to do
+ describe '#assign_to' do
let!(:project) { FactoryGirl.create :empty_project }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) }
- before { shared_runner.assign_to(project) }
+ before do
+ shared_runner.assign_to(project)
+ end
it { expect(shared_runner).to be_specific }
it { expect(shared_runner.projects).to eq([project]) }
it { expect(shared_runner.only_for?(project)).to be_truthy }
end
- describe :online do
+ describe '.online' do
subject { Ci::Runner.online }
before do
@@ -58,60 +60,269 @@ describe Ci::Runner, models: true do
it { is_expected.to eq([@runner2])}
end
- describe :online? do
+ describe '#online?' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared) }
subject { runner.online? }
context 'never contacted' do
- before { runner.contacted_at = nil }
+ before do
+ runner.contacted_at = nil
+ end
it { is_expected.to be_falsey }
end
context 'contacted long time ago time' do
- before { runner.contacted_at = 1.year.ago }
+ before do
+ runner.contacted_at = 1.year.ago
+ end
it { is_expected.to be_falsey }
end
context 'contacted 1s ago' do
- before { runner.contacted_at = 1.second.ago }
+ before do
+ runner.contacted_at = 1.second.ago
+ end
it { is_expected.to be_truthy }
end
end
- describe :status do
+ describe '#can_pick?' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ build.project.runners << runner
+ end
+
+ context 'when runner does not have tags' do
+ it 'can handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it 'cannot handle build with tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when runner has tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ end
+
+ shared_examples 'tagged build picker' do
+ it 'can handle build with matching tags' do
+ build.tag_list = ['bb']
+
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it 'cannot handle build without matching tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when runner can pick untagged jobs' do
+ it 'can handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it_behaves_like 'tagged build picker'
+ end
+
+ context 'when runner cannot pick untagged jobs' do
+ before do
+ runner.run_untagged = false
+ end
+
+ it 'cannot handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+
+ it_behaves_like 'tagged build picker'
+ end
+ end
+
+ context 'when runner is locked' do
+ before do
+ runner.locked = true
+ end
+
+ shared_examples 'locked build picker' do
+ context 'when runner cannot pick untagged jobs' do
+ before do
+ runner.run_untagged = false
+ end
+
+ it 'cannot handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ end
+
+ it 'cannot handle it for builds without matching tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+ end
+
+ context 'when serving the same project' do
+ it 'can handle it' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it_behaves_like 'locked build picker'
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ build.tag_list = ['bb']
+ end
+
+ it 'can handle it for matching tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+ end
+ end
+
+ context 'serving a different project' do
+ before do
+ runner.runner_projects.destroy_all
+ end
+
+ it 'cannot handle it' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+
+ it_behaves_like 'locked build picker'
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ build.tag_list = ['bb']
+ end
+
+ it 'cannot handle it for matching tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+ end
+ end
+ end
+
+ describe '#status' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
subject { runner.status }
context 'never connected' do
- before { runner.contacted_at = nil }
+ before do
+ runner.contacted_at = nil
+ end
it { is_expected.to eq(:not_connected) }
end
context 'contacted 1s ago' do
- before { runner.contacted_at = 1.second.ago }
+ before do
+ runner.contacted_at = 1.second.ago
+ end
it { is_expected.to eq(:online) }
end
context 'contacted long time ago' do
- before { runner.contacted_at = 1.year.ago }
+ before do
+ runner.contacted_at = 1.year.ago
+ end
it { is_expected.to eq(:offline) }
end
context 'inactive' do
- before { runner.active = false }
+ before do
+ runner.active = false
+ end
it { is_expected.to eq(:paused) }
end
end
+ describe '.assignable_for' do
+ let(:runner) { create(:ci_runner) }
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project) }
+
+ before do
+ project.runners << runner
+ end
+
+ context 'with shared runners' do
+ before do
+ runner.update(is_shared: true)
+ end
+
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does not give shared runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'with unlocked runner' do
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does give a specific runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to contain_exactly(runner) }
+ end
+ end
+
+ context 'with locked runner' do
+ before do
+ runner.update(locked: true)
+ end
+
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does not give a locked runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+ end
+
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
runner = FactoryGirl.create(:ci_runner)
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index beca8708c9d..ba02d5fe977 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -207,4 +207,16 @@ eos
expect(commit.participants).to include(note1.author, note2.author)
end
end
+
+ describe '#uri_type' do
+ it 'returns the URI type at the given path' do
+ expect(commit.uri_type('files/html')).to be(:tree)
+ expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
+ expect(commit.uri_type('files/js/application.js')).to be(:blob)
+ end
+
+ it "returns nil if the path doesn't exists" do
+ expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 8fb605fff8a..96397d7c8a9 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,8 +1,13 @@
require 'spec_helper'
describe CommitStatus, models: true do
- let(:pipeline) { FactoryGirl.create :ci_pipeline }
- let(:commit_status) { FactoryGirl.create :commit_status, pipeline: pipeline }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ end
+
+ let(:commit_status) { create(:commit_status, pipeline: pipeline) }
it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
@@ -13,7 +18,7 @@ describe CommitStatus, models: true do
it { is_expected.to delegate_method(:sha).to(:pipeline) }
it { is_expected.to delegate_method(:short_sha).to(:pipeline) }
-
+
it { is_expected.to respond_to :success? }
it { is_expected.to respond_to :failed? }
it { is_expected.to respond_to :running? }
@@ -116,7 +121,7 @@ describe CommitStatus, models: true do
it { is_expected.to be > 0.0 }
end
end
-
+
describe :latest do
subject { CommitStatus.latest.order(:id) }
@@ -198,4 +203,10 @@ describe CommitStatus, models: true do
end
end
end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(commit_status.commit).to eq project.commit
+ end
+ end
end
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
new file mode 100644
index 00000000000..98307876962
--- /dev/null
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe AccessRequestable do
+ describe 'Group' do
+ describe '#request_access' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ it { expect(group.request_access(user)).to be_a(GroupMember) }
+ it { expect(group.request_access(user).user).to eq(user) }
+ end
+
+ describe '#access_requested?' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ before { group.request_access(user) }
+
+ it { expect(group.members.request.exists?(user_id: user)).to be_truthy }
+ end
+ end
+
+ describe 'Project' do
+ describe '#request_access' do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ it { expect(project.request_access(user)).to be_a(ProjectMember) }
+ end
+
+ describe '#access_requested?' do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ before { project.request_access(user) }
+
+ it { expect(project.members.request.exists?(user_id: user)).to be_truthy }
+ end
+ end
+end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 47c3be673c5..7e9ab8940cf 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -5,6 +5,7 @@ describe Milestone, 'Milestoneish' do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -21,6 +22,7 @@ describe Milestone, 'Milestoneish' do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
describe '#closed_items_count' do
@@ -28,6 +30,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.closed_items_count(non_member)).to eq 2
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.closed_items_count(guest)).to eq 2
+ end
+
it 'should count confidential issues for author' do
expect(milestone.closed_items_count(author)).to eq 4
end
@@ -50,6 +56,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.total_items_count(non_member)).to eq 4
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.total_items_count(guest)).to eq 4
+ end
+
it 'should count confidential issues for author' do
expect(milestone.total_items_count(author)).to eq 7
end
@@ -85,6 +95,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.percent_complete(non_member)).to eq 50
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.percent_complete(guest)).to eq 50
+ end
+
it 'should count confidential issues for author' do
expect(milestone.percent_complete(author)).to eq 57
end
diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb
index 7e4ea0f2d66..a9f4ef9ee5e 100644
--- a/spec/models/concerns/participable_spec.rb
+++ b/spec/models/concerns/participable_spec.rb
@@ -37,6 +37,16 @@ describe Participable, models: true do
expect(participants).to include(user3)
end
+ it 'caches the raw list of participants' do
+ instance = model.new
+ user1 = build(:user)
+
+ expect(instance).to receive(:raw_participants).once
+
+ instance.participants(user1)
+ instance.participants(user1)
+ end
+
it 'supports attributes returning another Participable' do
other_model = Class.new { include Participable }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
new file mode 100644
index 00000000000..b273018707f
--- /dev/null
+++ b/spec/models/deployment_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Deployment, models: true do
+ subject { build(:deployment) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:environment) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:deployable) }
+
+ it { is_expected.to delegate_method(:name).to(:environment).with_prefix }
+ it { is_expected.to delegate_method(:commit).to(:project) }
+ it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) }
+
+ it { is_expected.to validate_presence_of(:ref) }
+ it { is_expected.to validate_presence_of(:sha) }
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
new file mode 100644
index 00000000000..7629af6a570
--- /dev/null
+++ b/spec/models/environment_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Environment, models: true do
+ let(:environment) { create(:environment) }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:deployments) }
+
+ it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
+
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
+ it { is_expected.to validate_length_of(:name).is_within(0..255) }
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index b0e76fec693..166a1dc4ddb 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -50,6 +50,7 @@ describe Event, models: true do
let(:project) { create(:empty_project, :public) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
@@ -61,6 +62,7 @@ describe Event, models: true do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
context 'issue event' do
@@ -71,6 +73,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -81,6 +84,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
@@ -93,6 +97,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -103,6 +108,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6fa16be7f04..2c19aa3f67f 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -5,7 +5,11 @@ describe Group, models: true do
describe 'associations' do
it { is_expected.to have_many :projects }
- it { is_expected.to have_many :group_members }
+ it { is_expected.to have_many(:group_members).dependent(:destroy) }
+ it { is_expected.to have_many(:users).through(:group_members) }
+ it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
+ it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
+ it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
end
describe 'modules' do
@@ -131,4 +135,58 @@ describe Group, models: true do
expect(described_class.search(group.path.upcase)).to eq([group])
end
end
+
+ describe '#has_owner?' do
+ before { @members = setup_group_members(group) }
+
+ it { expect(group.has_owner?(@members[:owner])).to be_truthy }
+ it { expect(group.has_owner?(@members[:master])).to be_falsey }
+ it { expect(group.has_owner?(@members[:developer])).to be_falsey }
+ it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
+ it { expect(group.has_owner?(@members[:guest])).to be_falsey }
+ it { expect(group.has_owner?(@members[:requester])).to be_falsey }
+ end
+
+ describe '#has_master?' do
+ before { @members = setup_group_members(group) }
+
+ it { expect(group.has_master?(@members[:owner])).to be_falsey }
+ it { expect(group.has_master?(@members[:master])).to be_truthy }
+ it { expect(group.has_master?(@members[:developer])).to be_falsey }
+ it { expect(group.has_master?(@members[:reporter])).to be_falsey }
+ it { expect(group.has_master?(@members[:guest])).to be_falsey }
+ 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),
+ master: create(:user),
+ developer: create(:user),
+ reporter: create(:user),
+ guest: create(:user),
+ requester: create(:user)
+ }
+
+ group.add_user(members[:owner], GroupMember::OWNER)
+ group.add_user(members[:master], GroupMember::MASTER)
+ group.add_user(members[:developer], GroupMember::DEVELOPER)
+ group.add_user(members[:reporter], GroupMember::REPORTER)
+ group.add_user(members[:guest], GroupMember::GUEST)
+ group.request_access(members[:requester])
+
+ members
+ end
end
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/key_spec.rb b/spec/models/key_spec.rb
index 26fbedbef2f..49cf3d8633a 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -26,7 +26,7 @@ describe Key, models: true do
end
end
- context "validation of uniqueness" do
+ context "validation of uniqueness (based on fingerprint uniqueness)" do
let(:user) { create(:user) }
it "accepts the key once" do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6e51730eecd..e9134a3d283 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -55,6 +55,80 @@ describe Member, models: true do
end
end
+ describe 'Scopes & finders' do
+ before do
+ project = create(:project)
+ group = create(:group)
+ @owner_user = create(:user).tap { |u| group.add_owner(u) }
+ @owner = group.members.find_by(user_id: @owner_user.id)
+
+ @master_user = create(:user).tap { |u| project.team << [u, :master] }
+ @master = project.members.find_by(user_id: @master_user.id)
+
+ ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
+
+ accepted_invite_user = build(:user)
+ ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
+
+ requested_user = create(:user).tap { |u| project.request_access(u) }
+ @requested_member = project.members.request.find_by(user_id: requested_user.id)
+
+ accepted_request_user = create(:user).tap { |u| project.request_access(u) }
+ @accepted_request_member = project.members.request.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
+ end
+
+ describe '.invite' do
+ it { expect(described_class.invite).not_to include @master }
+ it { expect(described_class.invite).to include @invited_member }
+ it { expect(described_class.invite).not_to include @accepted_invite_member }
+ it { expect(described_class.invite).not_to include @requested_member }
+ it { expect(described_class.invite).not_to include @accepted_request_member }
+ end
+
+ describe '.non_invite' do
+ it { expect(described_class.non_invite).to include @master }
+ it { expect(described_class.non_invite).not_to include @invited_member }
+ it { expect(described_class.non_invite).to include @accepted_invite_member }
+ it { expect(described_class.non_invite).to include @requested_member }
+ it { expect(described_class.non_invite).to include @accepted_request_member }
+ end
+
+ describe '.request' do
+ it { expect(described_class.request).not_to include @master }
+ it { expect(described_class.request).not_to include @invited_member }
+ it { expect(described_class.request).not_to include @accepted_invite_member }
+ it { expect(described_class.request).to include @requested_member }
+ it { expect(described_class.request).not_to include @accepted_request_member }
+ end
+
+ describe '.non_request' do
+ it { expect(described_class.non_request).to include @master }
+ it { expect(described_class.non_request).to include @invited_member }
+ it { expect(described_class.non_request).to include @accepted_invite_member }
+ it { expect(described_class.non_request).not_to include @requested_member }
+ it { expect(described_class.non_request).to include @accepted_request_member }
+ end
+
+ describe '.non_pending' do
+ it { expect(described_class.non_pending).to include @master }
+ it { expect(described_class.non_pending).not_to include @invited_member }
+ it { expect(described_class.non_pending).to include @accepted_invite_member }
+ it { expect(described_class.non_pending).not_to include @requested_member }
+ it { expect(described_class.non_pending).to include @accepted_request_member }
+ end
+
+ describe '.owners_and_masters' do
+ it { expect(described_class.owners_and_masters).to include @owner }
+ it { expect(described_class.owners_and_masters).to include @master }
+ it { expect(described_class.owners_and_masters).not_to include @invited_member }
+ it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
+ it { expect(described_class.owners_and_masters).not_to include @requested_member }
+ it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
+ end
+ end
+
describe "Delegate methods" do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
@@ -97,6 +171,44 @@ describe Member, models: true do
end
end
+ describe '#accept_request' do
+ let(:member) { create(:project_member, requested_at: Time.now.utc) }
+
+ it { expect(member.accept_request).to be_truthy }
+
+ it 'clears requested_at' do
+ member.accept_request
+
+ expect(member.requested_at).to be_nil
+ end
+
+ it 'calls #after_accept_request' do
+ expect(member).to receive(:after_accept_request)
+
+ member.accept_request
+ end
+ end
+
+ describe '#invite?' do
+ subject { create(:project_member, invite_email: "user@example.com", user: nil) }
+
+ it { is_expected.to be_invite }
+ end
+
+ describe '#request?' do
+ subject { create(:project_member, requested_at: Time.now.utc) }
+
+ it { is_expected.to be_request }
+ end
+
+ describe '#pending?' do
+ let(:invited_member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+ let(:requester) { create(:project_member, requested_at: Time.now.utc) }
+
+ it { expect(invited_member).to be_invite }
+ it { expect(requester).to be_pending }
+ end
+
describe "#accept_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 5424c9b9cba..18439cac2a4 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
describe GroupMember, models: true do
- context 'notification' do
+ describe 'notifications' do
describe "#after_create" do
it "should send email to user" do
membership = build(:group_member)
@@ -50,5 +50,21 @@ describe GroupMember, models: true do
@group_member.update_attribute(:access_level, GroupMember::OWNER)
end
end
+
+ describe '#after_accept_request' do
+ it 'calls NotificationService.accept_group_access_request' do
+ member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:new_group_member)
+
+ member.__send__(:after_accept_request)
+ end
+ end
+
+ describe '#real_source_type' do
+ subject { create(:group_member).real_source_type }
+
+ it { is_expected.to eq 'Group' }
+ end
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 9f13874b532..bbf65edb27c 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -33,6 +33,12 @@ describe ProjectMember, models: true do
it { is_expected.to include_module(Gitlab::ShellAdapter) }
end
+ describe '#real_source_type' do
+ subject { create(:project_member).real_source_type }
+
+ it { is_expected.to eq 'Project' }
+ end
+
describe "#destroy" do
let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) }
let(:project) { owner.project }
@@ -135,4 +141,16 @@ describe ProjectMember, models: true do
it { expect(@project_1.users).to be_empty }
it { expect(@project_2.users).to be_empty }
end
+
+ describe 'notifications' do
+ describe '#after_accept_request' do
+ it 'calls NotificationService.new_project_member' do
+ member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:new_project_member)
+
+ member.__send__(:after_accept_request)
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index f15e96714b2..285ab19cfaf 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -162,16 +162,23 @@ describe Note, models: true do
end
context "confidential issues" do
- let(:user) { create :user }
- let(:confidential_issue) { create(:issue, :confidential, author: user) }
- let(:confidential_note) { create :note, note: "Random", noteable: confidential_issue, project: confidential_issue.project }
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
it "returns notes with matching content if user can see the issue" do
expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note])
end
it "does not return notes with matching content if user can not see the issue" do
- user = create :user
+ user = create(:user)
+ expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
+ end
+
+ it "does not return notes with matching content for project members with guest role" do
+ user = create(:user)
+ project.team << [user, :guest]
expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
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/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index ec81f05fc7a..9ae461f8c2d 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -126,25 +126,25 @@ describe BambooService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a specific URL when response has no results' do
stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a build URL when bamboo_url has no trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
it 'returns a build URL when bamboo_url has a trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
end
@@ -192,7 +192,7 @@ describe BambooService, models: true do
end
end
- def service(bamboo_url: 'http://gitlab.com')
+ def service(bamboo_url: 'http://gitlab.com/bamboo')
described_class.create(
project: create(:empty_project),
properties: {
@@ -205,7 +205,7 @@ describe BambooService, models: true do
end
def stub_request(status: 200, body: nil, build_state: 'success')
- bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic'
+ bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}})
WebMock.stub_request(:get, bamboo_full_url).to_return(
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_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index 24a708ca849..474715d24c3 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -126,19 +126,19 @@ describe TeamcityService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has no trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has a trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
end
@@ -180,7 +180,7 @@ describe TeamcityService, models: true do
end
end
- def service(teamcity_url: 'http://gitlab.com')
+ def service(teamcity_url: 'http://gitlab.com/teamcity')
described_class.create(
project: create(:empty_project),
properties: {
@@ -193,7 +193,7 @@ describe TeamcityService, models: true do
end
def stub_request(status: 200, body: nil, build_status: 'success')
- teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
+ teamcity_full_url = 'http://mic:password@gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
WebMock.stub_request(:get, teamcity_full_url).to_return(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f3590f72cfe..53c8408633c 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -28,6 +28,8 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:environments).dependent(:destroy) }
+ it { is_expected.to have_many(:deployments).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
end
@@ -53,7 +55,6 @@ describe Project, models: true do
it { is_expected.to validate_length_of(:path).is_within(0..255) }
it { is_expected.to validate_length_of(:description).is_within(0..2000) }
it { is_expected.to validate_presence_of(:creator) }
- it { is_expected.to validate_length_of(:issues_tracker_id).is_within(0..255) }
it { is_expected.to validate_presence_of(:namespace) }
it 'should not allow new projects beyond user limits' do
@@ -90,11 +91,17 @@ describe Project, models: true do
it { is_expected.to respond_to(:repo_exists?) }
it { is_expected.to respond_to(:update_merge_requests) }
it { is_expected.to respond_to(:execute_hooks) }
- it { is_expected.to respond_to(:name_with_namespace) }
it { is_expected.to respond_to(:owner) }
it { is_expected.to respond_to(:path_with_namespace) }
end
+ describe '#name_with_namespace' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
+ it { expect(project.human_name).to eq project.name_with_namespace }
+ end
+
describe '#to_reference' do
let(:project) { create(:empty_project) }
@@ -213,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'
@@ -224,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
@@ -321,27 +344,6 @@ describe Project, models: true do
end
end
- describe :can_have_issues_tracker_id? do
- let(:project) { create(:project) }
- let(:ext_project) { create(:redmine_project) }
-
- it 'should be true for projects with external issues tracker if issues enabled' do
- expect(ext_project.can_have_issues_tracker_id?).to be_truthy
- end
-
- it 'should be false for projects with internal issue tracker if issues enabled' do
- expect(project.can_have_issues_tracker_id?).to be_falsey
- end
-
- it 'should be always false if issues disabled' do
- project.issues_enabled = false
- ext_project.issues_enabled = false
-
- expect(project.can_have_issues_tracker_id?).to be_falsey
- expect(ext_project.can_have_issues_tracker_id?).to be_falsey
- end
- end
-
describe :open_branches do
let(:project) { create(:project) }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index bacb17a8883..9262aeb6ed8 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -29,6 +29,9 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::REPORTER)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::REPORTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
@@ -64,50 +67,48 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::MASTER)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::MASTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
- describe :max_invited_level do
- let(:group) { create(:group) }
- let(:project) { create(:empty_project) }
-
- before do
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::DEVELOPER
- )
-
- group.add_user(master, Gitlab::Access::MASTER)
- group.add_user(reporter, Gitlab::Access::REPORTER)
- end
-
- it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
- it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
- it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
- end
-
- describe :max_member_access do
- let(:group) { create(:group) }
- let(:project) { create(:empty_project) }
-
- before do
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::DEVELOPER
- )
-
- group.add_user(master, Gitlab::Access::MASTER)
- group.add_user(reporter, Gitlab::Access::REPORTER)
+ describe '#find_member' do
+ context 'personal project' do
+ let(:project) { create(:empty_project) }
+ let(:requester) { create(:user) }
+
+ before do
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ project.request_access(requester)
+ end
+
+ it { expect(project.team.find_member(master.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(reporter.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(guest.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(nonmember.id)).to be_nil }
+ it { expect(project.team.find_member(requester.id)).to be_nil }
end
- it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
- it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
- it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
-
- it "does not have an access" do
- project.namespace.update(share_with_group_lock: true)
- expect(project.team.max_member_access(master.id)).to be_nil
- expect(project.team.max_member_access(reporter.id)).to be_nil
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+ let(:requester) { create(:user) }
+
+ before do
+ group.add_master(master)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+ group.request_access(requester)
+ end
+
+ it { expect(project.team.find_member(master.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(reporter.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(guest.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(nonmember.id)).to be_nil }
+ it { expect(project.team.find_member(requester.id)).to be_nil }
end
end
@@ -132,4 +133,69 @@ describe ProjectTeam, models: true do
expect(project.team.human_max_access(user.id)).to eq 'Owner'
end
end
+
+ describe '#max_member_access' do
+ let(:requester) { create(:user) }
+
+ context 'personal project' do
+ let(:project) { create(:empty_project) }
+
+ context 'when project is not shared with group' do
+ before do
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ project.request_access(requester)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ end
+
+ context 'when project is shared with group' do
+ before do
+ group = create(:group)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER)
+
+ group.add_master(master)
+ group.add_reporter(reporter)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+
+ context 'but share_with_group_lock is true' do
+ before { project.namespace.update(share_with_group_lock: true) }
+
+ it { expect(project.team.max_member_access(master.id)).to be_nil }
+ it { expect(project.team.max_member_access(reporter.id)).to be_nil }
+ end
+ end
+ end
+
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+
+ before do
+ group.add_master(master)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+ group.request_access(requester)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ end
+ end
end
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/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
new file mode 100644
index 00000000000..2e65e7f1920
--- /dev/null
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -0,0 +1,198 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let!(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ let!(:note) { create(:note, project: project, noteable: issue) }
+
+ before { project.team << [user, :master] }
+
+ describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do
+ context 'on an issue' do
+ it "returns an array of award_emoji" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(award_emoji.name)
+ end
+
+ it "should return a 404 error when issue id not found" do
+ get api("/projects/#{project.id}/issues/12345/award_emoji", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'on a merge request' do
+ it "returns an array of award_emoji" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(downvote.name)
+ end
+ end
+
+ context 'when the user has no access' do
+ it 'returns a status code 404' do
+ user1 = create(:user)
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji", user1)
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+
+ it 'returns an array of award emoji' do
+ get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(rocket.name)
+ end
+ end
+
+
+ describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do
+ context 'on an issue' do
+ it "returns the award emoji" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(award_emoji.name)
+ expect(json_response['awardable_id']).to eq(issue.id)
+ expect(json_response['awardable_type']).to eq("Issue")
+ end
+
+ it "returns a 404 error if the award is not found" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'on a merge request' do
+ it 'returns the award emoji' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(downvote.name)
+ expect(json_response['awardable_id']).to eq(merge_request.id)
+ expect(json_response['awardable_type']).to eq("MergeRequest")
+ end
+ end
+
+ context 'when the user has no access' do
+ it 'returns a status code 404' do
+ user1 = create(:user)
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user1)
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') }
+
+ it 'returns an award emoji' do
+ get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).not_to be_an Array
+ expect(json_response['name']).to eq(rocket.name)
+ end
+ end
+
+ describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
+ context "on an issue" do
+ it "creates a new award emoji" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('blowfish')
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+
+ it "should return a 400 bad request error if the name is not given" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
+
+ expect(response.status).to eq(400)
+ end
+
+ it "should return a 401 unauthorized error if the user is not authenticated" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
+ it 'creates a new award emoji' do
+ expect do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ end.to change { note.award_emoji.count }.from(0).to(1)
+
+ expect(response.status).to eq(201)
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+ end
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
+ context 'when the awardable is an Issue' do
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/#{award_emoji.id}", user)
+ end.to change { issue.award_emoji.count }.from(1).to(0)
+
+ expect(response.status).to eq(200)
+ end
+
+ it 'returns a 404 error when the award emoji can not be found' do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/award_emoji/12345", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when the awardable is a Merge Request' do
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/award_emoji/#{downvote.id}", user)
+ end.to change { merge_request.award_emoji.count }.from(1).to(0)
+
+ expect(response.status).to eq(200)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes/12345", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
+ let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji/#{rocket.id}", user)
+ end.to change { note.award_emoji.count }.from(1).to(0)
+
+ expect(response.status).to eq(200)
+ end
+ end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 6cb7be188ef..47e9253a10c 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -9,8 +9,8 @@ describe API::API, api: true do
let!(:project) { create(:project, creator_id: user.id) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) }
let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
- let(:pipeline) { create(:ci_pipeline, project: project)}
- let(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
@@ -23,6 +23,11 @@ describe API::API, api: true do
expect(json_response).to be_an Array
end
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
context 'filter project with one scope element' do
let(:query) { 'scope=pending' }
@@ -132,7 +137,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/builds/:build_id/trace' do
let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
-
+
before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) }
context 'authorized user' do
@@ -241,4 +246,30 @@ describe API::API, api: true do
end
end
end
+
+ describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
+ end
+
+ context 'artifacts did not expire' do
+ let(:build) do
+ create(:ci_build, :trace, :artifacts, :success,
+ project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'keeps artifacts' do
+ expect(response.status).to eq 200
+ expect(build.reload.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'no artifacts' do
+ let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'responds with not found' do
+ expect(response.status).to eq 404
+ end
+ end
+ end
end
diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb
deleted file mode 100644
index aab2d8c81b9..00000000000
--- a/spec/requests/api/gitignores_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'spec_helper'
-
-describe API::Gitignores, api: true do
- include ApiHelpers
-
- describe 'Entity Gitignore' do
- before { get api('/gitignores/Ruby') }
-
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
- end
-
- describe 'Entity GitignoresList' do
- before { get api('/gitignores') }
-
- it { expect(json_response.first['name']).not_to be_nil }
- it { expect(json_response.first['content']).to be_nil }
- end
-
- describe 'GET /gitignores' do
- it 'returns a list of available license templates' do
- get api('/gitignores')
-
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
- end
- end
-end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index bb926172593..59e557c5b2a 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -5,6 +5,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
@@ -41,7 +42,10 @@ describe API::API, api: true do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
- before { project.team << [user, :reporter] }
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
describe "GET /issues" do
context "when unauthenticated" do
@@ -144,6 +148,14 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
+ it 'should return project issues without confidential issues for project members with guest role' do
+ get api("#{base_url}/issues", guest)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
it 'should return project confidential issues for author' do
get api("#{base_url}/issues", author)
expect(response.status).to eq(200)
@@ -278,6 +290,11 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+ it "should return 404 for project members with guest role" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+ expect(response.status).to eq(404)
+ end
+
it "should return confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
expect(response.status).to eq(200)
@@ -413,6 +430,12 @@ describe API::API, api: true do
expect(response.status).to eq(403)
end
+ it "should return 403 for project members with guest role" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+ expect(response.status).to eq(403)
+ end
+
it "should update a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
title: 'updated title'
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 241995041bb..0154d1c62cc 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -146,6 +146,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+
before do
public_project.team << [user, :developer]
milestone.issues << issue << confidential_issue
@@ -160,6 +161,18 @@ describe API::API, api: true do
expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
end
+ it 'does not return confidential issues to team members with guest role' do
+ member = create(:user)
+ project.team << [member, :guest]
+
+ get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ end
+
it 'does not return confidential issues to regular users' do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 2a2ac8710ee..01eb4b44b83 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -438,14 +438,6 @@ describe API::API, api: true do
to eq(Gitlab::Access::MASTER)
expect(json_response.first['permissions']['group_access']).to be_nil
end
-
- it 'contains notification level information' do
- get api("/projects", user)
-
- expect(response.status).to eq(200)
- expect(json_response.first['permissions']['project_access']['notification_level']['level']).to eq(NotificationSetting.levels[:global])
- expect(json_response.first['permissions']['project_access']['notification_level'].keys).to include('events')
- end
end
context 'personal project' do
@@ -473,14 +465,6 @@ describe API::API, api: true do
expect(json_response['permissions']['group_access']['access_level']).
to eq(Gitlab::Access::OWNER)
end
-
- it 'shows notification level information' do
- get api("/projects/#{project2.id}", user)
-
- expect(response.status).to eq(200)
- expect(json_response['permissions']['group_access']['notification_level']['level']).to eq(NotificationSetting.levels[:global])
- expect(json_response['permissions']['group_access']['notification_level'].keys).to include('events')
- end
end
end
end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 73ae8ef631c..b4c826522a5 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -187,14 +187,16 @@ describe API::Runners, api: true do
update_runner(shared_runner.id, admin, description: "#{description}_updated",
active: !active,
tag_list: ['ruby2.1', 'pgsql', 'mysql'],
- run_untagged: 'false')
+ run_untagged: 'false',
+ locked: 'true')
shared_runner.reload
expect(response.status).to eq(200)
expect(shared_runner.description).to eq("#{description}_updated")
expect(shared_runner.active).to eq(!active)
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
- expect(shared_runner.run_untagged?).to be false
+ expect(shared_runner.run_untagged?).to be(false)
+ expect(shared_runner.locked?).to be(true)
end
end
@@ -360,11 +362,13 @@ describe API::Runners, api: true do
describe 'POST /projects/:id/runners' do
context 'authorized user' do
- it 'should enable specific runner' do
- specific_runner2 = create(:ci_runner).tap do |runner|
+ let(:specific_runner2) do
+ create(:ci_runner).tap do |runner|
create(:ci_runner_project, runner: runner, project: project2)
end
+ end
+ it 'should enable specific runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change{ project.runners.count }.by(+1)
@@ -375,7 +379,17 @@ describe API::Runners, api: true do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
end.to change{ project.runners.count }.by(0)
- expect(response.status).to eq(201)
+ expect(response.status).to eq(409)
+ end
+
+ it 'should not enable locked runner' do
+ specific_runner2.update(locked: true)
+
+ expect do
+ post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
+ end.to change{ project.runners.count }.by(0)
+
+ expect(response.status).to eq(403)
end
it 'should not enable shared runner' 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/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
new file mode 100644
index 00000000000..a6d5ade3013
--- /dev/null
+++ b/spec/requests/api/templates_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe API::Templates, api: true do
+ include ApiHelpers
+
+ describe 'the Template Entity' do
+ before { get api('/gitignores/Ruby') }
+
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
+
+ describe 'the TemplateList Entity' do
+ before { get api('/gitignores') }
+
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
+
+ context 'requesting gitignores' do
+ describe 'GET /gitignores' do
+ it 'returns a list of available gitignore templates' do
+ get api('/gitignores')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
+ end
+ end
+
+ context 'requesting gitlab-ci-ymls' do
+ describe 'GET /gitlab_ci_ymls' do
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api('/gitlab_ci_ymls')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
+ end
+ end
+ end
+
+ describe 'GET /gitlab_ci_ymls/Ruby' do
+ it 'adds a disclaimer on the top' do
+ get api('/gitlab_ci_ymls/Ruby')
+
+ expect(response.status).to eq(200)
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
+ end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index e8508f8f950..7e50bea90d1 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -364,6 +364,42 @@ describe Ci::API::API do
end
end
+ context 'with an expire date' do
+ let!(:artifacts) { file_upload }
+
+ let(:post_data) do
+ { 'file.path' => artifacts.path,
+ 'file.name' => artifacts.original_filename,
+ 'expire_in' => expire_in }
+ end
+
+ before do
+ post(post_url, post_data, headers_with_token)
+ end
+
+ context 'with an expire_in given' do
+ let(:expire_in) { '7 days' }
+
+ it 'updates when specified' do
+ build.reload
+ expect(response.status).to eq(201)
+ expect(json_response['artifacts_expire_at']).not_to be_empty
+ expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
+ end
+ end
+
+ context 'with no expire_in given' do
+ let(:expire_in) { nil }
+
+ it 'ignores if not specified' do
+ build.reload
+ expect(response.status).to eq(201)
+ expect(json_response['artifacts_expire_at']).to be_nil
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+ end
+
context "artifacts file is too large" do
it "should fail to post too large artifact" do
stub_application_setting(max_artifacts_size: 0)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index c44a4a7a1fc..fd26ca97818 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -340,7 +340,7 @@ describe 'Git HTTP requests', lib: true do
end
end
- context "when the file exists" do
+ context "when the file does not exist" do
before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
it "returns not found" do
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/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
new file mode 100644
index 00000000000..654e441f3cd
--- /dev/null
+++ b/spec/services/create_deployment_service_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe CreateDeploymentService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ let(:service) { described_class.new(project, user, params) }
+
+ describe '#execute' do
+ let(:params) do
+ { environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ }
+ end
+
+ subject { service.execute }
+
+ context 'when no environments exist' do
+ it 'does create a new environment' do
+ expect { subject }.to change { Environment.count }.by(1)
+ end
+
+ it 'does create a deployment' do
+ expect(subject).to be_persisted
+ end
+ end
+
+ context 'when environment exist' do
+ before { create(:environment, project: project, name: 'production') }
+
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'does create a deployment' do
+ expect(subject).to be_persisted
+ end
+ end
+
+ context 'for environment with invalid name' do
+ let(:params) do
+ { environment: 'name with spaces',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ }
+ end
+
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'does not create a deployment' do
+ expect(subject).not_to be_persisted
+ end
+ end
+ end
+
+ describe 'processing of builds' do
+ let(:environment) { nil }
+
+ shared_examples 'does not create environment and deployment' do
+ it 'does not create a new environment' do
+ expect { subject }.not_to change { Environment.count }
+ end
+
+ it 'does not create a new deployment' do
+ expect { subject }.not_to change { Deployment.count }
+ end
+
+ it 'does not call a service' do
+ expect_any_instance_of(described_class).not_to receive(:execute)
+ subject
+ end
+ end
+
+ shared_examples 'does create environment and deployment' do
+ it 'does create a new environment' do
+ expect { subject }.to change { Environment.count }.by(1)
+ end
+
+ it 'does create a new deployment' do
+ expect { subject }.to change { Deployment.count }.by(1)
+ end
+
+ it 'does call a service' do
+ expect_any_instance_of(described_class).to receive(:execute)
+ subject
+ end
+ end
+
+ context 'without environment specified' do
+ let(:build) { create(:ci_build, project: project) }
+
+ it_behaves_like 'does not create environment and deployment' do
+ subject { build.success }
+ end
+ end
+
+ context 'when environment is specified' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production') }
+
+ context 'when build succeeds' do
+ it_behaves_like 'does create environment and deployment' do
+ subject { build.success }
+ end
+ end
+
+ context 'when build fails' do
+ it_behaves_like 'does not create environment and deployment' do
+ subject { build.drop }
+ end
+ 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/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
new file mode 100644
index 00000000000..2395445e7fd
--- /dev/null
+++ b/spec/services/members/destroy_service_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe Members::DestroyService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:member) { create(:project_member, source: project) }
+
+ context 'when member is nil' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not destroy the member' do
+ expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when current user cannot destroy the given member' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not destroy the member' do
+ expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when current user can destroy the given member' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'destroys the member' do
+ destroy_member(member, user)
+
+ expect(member).to be_destroyed
+ end
+
+ context 'when the given member is a requester' do
+ before do
+ member.update_column(:requested_at, Time.now)
+ end
+
+ it 'calls Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
+
+ destroy_member(member, user)
+ end
+
+ context 'when current user is the member' do
+ it 'does not call Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
+
+ destroy_member(member, member.user)
+ end
+ end
+
+ context 'when current user is the member and ' do
+ it 'does not call Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
+
+ destroy_member(member, member.user)
+ end
+ end
+ end
+ end
+
+ def destroy_member(member, user)
+ Members::DestroyService.new(member, user).execute
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 05cbb354cf4..776a6ab5edb 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -140,16 +140,15 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") }
- before do
- end
-
it 'filters out users that can not read the issue' do
project.team << [member, :developer]
+ project.team << [guest, :guest]
expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
@@ -158,6 +157,7 @@ describe NotificationService, services: true do
notification.new_note(note)
should_not_email(non_member)
+ should_not_email(guest)
should_not_email(guest_watcher)
should_email(author)
should_email(assignee)
@@ -345,17 +345,20 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label = create(:label, issues: [confidential_issue])
label.toggle_subscription(non_member)
label.toggle_subscription(author)
label.toggle_subscription(assignee)
label.toggle_subscription(member)
+ label.toggle_subscription(guest)
label.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
@@ -365,6 +368,7 @@ describe NotificationService, services: true do
should_not_email(@u_guest_watcher)
should_not_email(non_member)
should_not_email(author)
+ should_not_email(guest)
should_email(assignee)
should_email(member)
should_email(admin)
@@ -373,6 +377,7 @@ describe NotificationService, services: true do
end
describe '#reassigned_issue' do
+
before do
update_custom_notification(:reassign_issue, @u_guest_custom, project)
update_custom_notification(:reassign_issue, @u_custom_global)
@@ -529,6 +534,7 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
let!(:label_1) { create(:label, issues: [confidential_issue]) }
@@ -536,11 +542,13 @@ describe NotificationService, services: true do
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label_2.toggle_subscription(non_member)
label_2.toggle_subscription(author)
label_2.toggle_subscription(assignee)
label_2.toggle_subscription(member)
+ label_2.toggle_subscription(guest)
label_2.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
@@ -548,6 +556,7 @@ describe NotificationService, services: true do
notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
should_not_email(non_member)
+ should_not_email(guest)
should_email(author)
should_email(assignee)
should_email(member)
@@ -557,6 +566,7 @@ describe NotificationService, services: true do
end
describe '#close_issue' do
+
before do
update_custom_notification(:close_issue, @u_guest_custom, project)
update_custom_notification(:close_issue, @u_custom_global)
@@ -678,7 +688,6 @@ describe NotificationService, services: true do
update_custom_notification(:new_merge_request, @u_custom_global)
end
-
it do
notification.new_merge_request(merge_request, @u_disabled)
@@ -871,6 +880,7 @@ describe NotificationService, services: true do
end
describe '#merged_merge_request' do
+
before do
update_custom_notification(:merge_merge_request, @u_guest_custom, project)
update_custom_notification(:merge_merge_request, @u_custom_global)
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 6108c26a78b..0971fec2e9f 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -33,6 +33,18 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ autocomplete = described_class.new(project, non_member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
it 'should list project confidential issues for author' do
autocomplete = described_class.new(project, author)
issues = autocomplete.issues.map(&:iid)
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 489c920f19f..b4522536724 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -5,13 +5,15 @@ describe TodoService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') }
+ let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
+ project.team << [guest, :guest]
project.team << [author, :developer]
project.team << [member, :developer]
project.team << [john_doe, :developer]
@@ -41,18 +43,20 @@ describe TodoService, services: true do
service.new_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, 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: non_member, target: issue, action: Todo::MENTIONED)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.new_issue(confidential_issue, john_doe)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
@@ -81,6 +85,7 @@ describe TodoService, services: true do
service.update_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
@@ -92,27 +97,36 @@ describe TodoService, services: true do
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.update_issue(confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
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)
+ 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)
+ 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
+
+ it 'does not raise an error when description not change' do
+ issue.update(title: 'Sample')
+
+ expect { service.update_issue(issue, author) }.not_to raise_error
+ end
end
end
@@ -159,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
@@ -192,18 +248,20 @@ describe TodoService, services: true do
service.new_note(note, john_doe)
should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
- it 'does not create todo for non project members when leaving a note on a confidential issue' do
+ it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do
service.new_note(note_on_confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
@@ -220,6 +278,14 @@ describe TodoService, services: true do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end
end
+
+ describe '#mark_todo' do
+ it 'creates a todo from a issue' do
+ service.mark_todo(unassigned_issue, author)
+
+ should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
+ end
+ end
end
describe 'Merge Requests' do
@@ -245,6 +311,7 @@ describe TodoService, services: true do
service.new_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, 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: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -256,6 +323,7 @@ describe TodoService, services: true do
service.update_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -267,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
@@ -351,6 +427,26 @@ describe TodoService, services: true do
expect(second_todo.reload).not_to be_done
end
end
+
+ describe '#mark_todo' do
+ it 'creates a todo from a merge request' do
+ service.mark_todo(mr_unassigned, author)
+
+ should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED)
+ end
+ 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 = {})
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/support/test_env.rb b/spec/support/test_env.rb
index 71664bb192e..498bd4bf800 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -16,6 +16,7 @@ module TestEnv
'master' => '5937ac0',
"'test'" => 'e56497b',
'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
new file mode 100644
index 00000000000..7d6668920c0
--- /dev/null
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe ExpireBuildArtifactsWorker do
+ include RepoHelpers
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before { build }
+
+ subject! { worker.perform }
+
+ context 'with expired artifacts' do
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'does expire' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+
+ 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
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ 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
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ 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
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'is still expired' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 1abd87d7d33..b5e1fdb8ded 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -9,7 +9,7 @@ describe MergeWorker do
before do
source_project.team << [author, :master]
- source_project.repository.expire_branch_names
+ source_project.repository.expire_branches_cache
end
it 'clears cache of source repo after removing source branch' do
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 5a03bb77ebd..05e07789dac 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -4,6 +4,26 @@ require 'fileutils'
describe RepositoryCheck::SingleRepositoryWorker do
subject { described_class.new }
+ it 'passes when the project has no push events' do
+ project = create(:project_empty_repo, wiki_enabled: false)
+ project.events.destroy_all
+ break_repo(project)
+
+ subject.perform(project.id)
+
+ expect(project.reload.last_repository_check_failed).to eq(false)
+ end
+
+ it 'fails when the project has push events and a broken repository' do
+ project = create(:project_empty_repo)
+ create_push_event(project)
+ break_repo(project)
+
+ subject.perform(project.id)
+
+ expect(project.reload.last_repository_check_failed).to eq(true)
+ end
+
it 'fails if the wiki repository is broken' do
project = create(:project_empty_repo, wiki_enabled: true)
project.create_wiki
@@ -39,6 +59,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
it 'does not create a wiki if the main repo does not exist at all' do
project = create(:project_empty_repo)
+ create_push_event(project)
FileUtils.rm_rf(project.repository.path_to_repo)
FileUtils.rm_rf(wiki_path(project))
@@ -54,4 +75,12 @@ describe RepositoryCheck::SingleRepositoryWorker do
def wiki_path(project)
project.wiki.repository.path_to_repo
end
+
+ def create_push_event(project)
+ project.events.create(action: Event::PUSHED, author_id: create(:user).id)
+ end
+
+ def break_repo(project)
+ FileUtils.rm_rf(File.join(project.repository.path_to_repo, 'objects'))
+ end
end
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
index 665ec20f224..801fa31b45d 100644
--- a/spec/workers/stuck_ci_builds_worker_spec.rb
+++ b/spec/workers/stuck_ci_builds_worker_spec.rb
@@ -2,6 +2,7 @@ require "spec_helper"
describe StuckCiBuildsWorker do
let!(:build) { create :ci_build }
+ let(:worker) { described_class.new }
subject do
build.reload
@@ -16,13 +17,13 @@ describe StuckCiBuildsWorker do
it 'gets dropped if it was updated over 2 days ago' do
build.update!(updated_at: 2.days.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq('failed')
end
it "is still #{status}" do
build.update!(updated_at: 1.minute.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq(status)
end
end
@@ -36,9 +37,21 @@ describe StuckCiBuildsWorker do
it "is still #{status}" do
build.update!(updated_at: 2.days.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq(status)
end
end
end
+
+ context "for deleted project" do
+ before do
+ build.update!(status: :running, updated_at: 2.days.ago)
+ build.project.update(pending_delete: true)
+ end
+
+ it "does not drop build" do
+ expect_any_instance_of(Ci::Build).not_to receive(:drop)
+ worker.perform
+ end
+ end
end
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;
+}));
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index a8368751267..f6b286cea98 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -2,7 +2,7 @@
*.apk
*.ap_
-# Files for the Dalvik VM
+# Files for the ART/Dalvik VM
*.dex
# Java class files
@@ -34,6 +34,7 @@ captures/
# Intellij
*.iml
+.idea/workspace.xml
# Keystore files
*.jks
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index b8bd0267bdf..4581ef2eeef 100644
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
@@ -15,6 +15,7 @@
# Fortran module files
*.mod
+*.smod
# Compiled Static libraries
*.lai
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
index b558e9afa6d..0cc7e4b5275 100644
--- a/vendor/gitignore/CMake.gitignore
+++ b/vendor/gitignore/CMake.gitignore
@@ -4,3 +4,4 @@ CMakeScripts
Makefile
cmake_install.cmake
install_manifest.txt
+CTestTestfile.cmake
diff --git a/vendor/gitignore/D.gitignore b/vendor/gitignore/D.gitignore
index b4433f8a512..74b926fc901 100644
--- a/vendor/gitignore/D.gitignore
+++ b/vendor/gitignore/D.gitignore
@@ -18,3 +18,7 @@
.dub
docs.json
__dummy.html
+docs/
+
+# Code coverage
+*.lst
diff --git a/vendor/gitignore/Global/Bazaar.gitignore b/vendor/gitignore/Global/Bazaar.gitignore
new file mode 100644
index 00000000000..3cbbcbd11ec
--- /dev/null
+++ b/vendor/gitignore/Global/Bazaar.gitignore
@@ -0,0 +1,2 @@
+.bzr/
+.bzrignore
diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/OSX.gitignore
index 660b31353e8..5972fe50f66 100644
--- a/vendor/gitignore/Global/OSX.gitignore
+++ b/vendor/gitignore/Global/OSX.gitignore
@@ -1,4 +1,4 @@
-.DS_Store
+*.DS_Store
.AppleDouble
.LSOverride
@@ -15,6 +15,7 @@ Icon
.TemporaryItems
.Trashes
.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
diff --git a/vendor/gitignore/Global/README.md b/vendor/gitignore/Global/README.md
new file mode 100644
index 00000000000..06b6649bd9a
--- /dev/null
+++ b/vendor/gitignore/Global/README.md
@@ -0,0 +1,10 @@
+## Globally Useful gitignores
+
+This directory contains globally useful gitignores,
+e.g. OS-specific and editor specific.
+
+For more on global gitignores:
+<https://help.github.com/articles/ignoring-files/#create-a-global-gitignore>
+
+And a good blog post about 'em:
+<http://augustl.com/blog/2009/global_gitignores>
diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore
index 1d4e6137591..69c8c2b29ce 100644
--- a/vendor/gitignore/Global/SublimeText.gitignore
+++ b/vendor/gitignore/Global/SublimeText.gitignore
@@ -12,3 +12,16 @@
# sftp configuration file
sftp-config.json
+
+# Package control specific files
+Package Control.last-run
+Package Control.ca-list
+Package Control.ca-bundle
+Package Control.system-ca-bundle
+Package Control.cache/
+Package Control.ca-certs/
+bh_unicode_properties.cache
+
+# Sublime-github package stores a github token in this file
+# https://packagecontrol.io/packages/sublime-github
+GitHub.sublime-settings
diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore
index 096abdd90b3..a4ee41ab62b 100644
--- a/vendor/gitignore/Haskell.gitignore
+++ b/vendor/gitignore/Haskell.gitignore
@@ -16,3 +16,4 @@ cabal.sandbox.config
*.hp
*.eventlog
.stack-work/
+cabal.project.local
diff --git a/vendor/gitignore/Julia.gitignore b/vendor/gitignore/Julia.gitignore
new file mode 100644
index 00000000000..381e0b6d252
--- /dev/null
+++ b/vendor/gitignore/Julia.gitignore
@@ -0,0 +1,4 @@
+*.jl.cov
+*.jl.*.cov
+*.jl.mem
+deps/deps.jl
diff --git a/vendor/gitignore/LICENSE b/vendor/gitignore/LICENSE
new file mode 100644
index 00000000000..b8a103ac9b1
--- /dev/null
+++ b/vendor/gitignore/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index c491fa2bc6f..1cd717b6921 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -7,7 +7,6 @@ app/storage/
# Laravel 5 & Lumen specific
bootstrap/cache/
-storage/
.env.*.php
.env.php
.env
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 3020bc327a7..86f21d8e0ff 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -24,6 +24,8 @@ xcuserdata/
## Obj-C/Swift specific
*.hmap
*.ipa
+*.dSYM.zip
+*.dSYM
# CocoaPods
#
@@ -49,3 +51,10 @@ Carthage/Build
fastlane/report.xml
fastlane/screenshots
+
+#Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore
index fa24b2efee8..c7659c24f38 100644
--- a/vendor/gitignore/Qt.gitignore
+++ b/vendor/gitignore/Qt.gitignore
@@ -34,5 +34,5 @@ Makefile*
*.qmlproject.user.*
# QtCtreator CMake
-CMakeLists.txt.user
+CMakeLists.txt.user*
diff --git a/vendor/gitignore/README.md b/vendor/gitignore/README.md
deleted file mode 100644
index 43131e815cc..00000000000
--- a/vendor/gitignore/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# .gitignore templates
-
-This directory contains language-specific .gitignore templates that are used by GitLab.
-
-These files were automatically pulled from [this repository](https://github.com/github/gitignore).
-Please submit pull requests to that repository. There is no need to edit the files in this directory.
-
-## Bulk Update
-
-To update this directory with the latest changes in the repository, run:
-
-```sh
-bundle exec rake gitlab:update_gitignore
-```
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index 2121e0a8038..d8c256c1925 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -16,6 +16,10 @@ pickle-email-*.html
config/initializers/secret_token.rb
config/secrets.yml
+# dotenv
+# TODO Comment out this rule if environment variables can be committed
+.env
+
## Environment normalization:
/.bundle
/vendor/bundle
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 8a29fa52af4..2c22487b5e3 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -24,6 +24,8 @@ xcuserdata/
## Obj-C/Swift specific
*.hmap
*.ipa
+*.dSYM.zip
+*.dSYM
## Playgrounds
timeline.xctimeline
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index 75b1186b0af..be0e4913c3a 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -37,6 +37,7 @@
*.suo
*.opensdf
*.sdf
+*.VC.db
*.VC.opendb
# Precompiled Assets
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index f1e3d20e056..67acbf42f5e 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -42,6 +42,7 @@ dlldata.c
# DNX
project.lock.json
+project.fragment.lock.json
artifacts/
*_i.c
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
new file mode 100644
index 00000000000..396d3f1b042
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -0,0 +1,7 @@
+# Official docker image.
+image: docker:latest
+
+build:
+ stage: build
+ script:
+ - docker build -t test .
diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
new file mode 100644
index 00000000000..0b329aaf1c4
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
@@ -0,0 +1,18 @@
+# This template uses the non default language docker image
+# The image already has Hex installed. You might want to consider to use `elixir:latest`
+image: trenpixster/elixir:latest
+
+# Pic zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+before_script:
+ - mix deps.get
+
+mix:
+ script:
+ - mix test
diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE
new file mode 100644
index 00000000000..80f7b87b6c0
--- /dev/null
+++ b/vendor/gitlab-ci-yml/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 GitLab.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml
new file mode 100644
index 00000000000..e5bce3503f3
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml
@@ -0,0 +1,27 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/node/tags/
+image: node:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - node_modules/
+
+test_async:
+ script:
+ - npm install
+ - node ./specs/start.js ./specs/async.spec.js
+
+test_db:
+ script:
+ - npm install
+ - node ./specs/start.js ./specs/db-postgres.spec.js
diff --git a/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml
new file mode 100644
index 00000000000..7fcc0b436b5
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml
@@ -0,0 +1,16 @@
+# Full project: https://gitlab.com/pages/brunch
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules/
+
+ script:
+ - npm install -g brunch
+ - brunch build --production
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml
new file mode 100644
index 00000000000..791afdd23f1
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml
@@ -0,0 +1,13 @@
+# Full project: https://gitlab.com/pages/doxygen
+image: alpine
+
+pages:
+ script:
+ - apk update && apk add doxygen
+ - doxygen doxygen/Doxyfile
+ - mv doxygen/documentation/html/ public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml
new file mode 100644
index 00000000000..dd3ef149668
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml
@@ -0,0 +1,16 @@
+# Full project: https://gitlab.com/pages/harp
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules
+
+ script:
+ - npm install -g harp
+ - harp compile ./ public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml
new file mode 100644
index 00000000000..b468d79bcad
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml
@@ -0,0 +1,25 @@
+# Full project: https://gitlab.com/pages/hexo
+image: python:2.7
+
+cache:
+ paths:
+ - vendor/
+
+test:
+ stage: test
+ script:
+ - pip install hyde
+ - hyde gen
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - pip install hyde
+ - hyde gen -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml
new file mode 100644
index 00000000000..249a168aa33
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/plain-html
+pages:
+ stage: deploy
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml
new file mode 100644
index 00000000000..45df6975259
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml
@@ -0,0 +1,11 @@
+# Full project: https://gitlab.com/pages/hugo
+image: publysher/hugo
+
+pages:
+ script:
+ - hugo
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml
new file mode 100644
index 00000000000..f5b40f2b9f1
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml
@@ -0,0 +1,25 @@
+# Full project: https://gitlab.com/pages/hyde
+image: python:2.7
+
+cache:
+ paths:
+ - vendor/
+
+test:
+ stage: test
+ script:
+ - pip install hyde
+ - hyde gen
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - pip install hyde
+ - hyde gen -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml
new file mode 100644
index 00000000000..36918fc005a
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml
@@ -0,0 +1,24 @@
+# Full project: https://gitlab.com/pages/jekyll
+image: ruby:2.3
+
+test:
+ stage: test
+ script:
+ - gem install jekyll
+ - jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - gem install jekyll
+ - jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml
new file mode 100644
index 00000000000..c5c44a5d86c
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/hyde
+image: python:2.7
+
+pages:
+ script:
+ - pip install lektor
+ - lektor build --output-path public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml
new file mode 100644
index 00000000000..50e8b7ccd46
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml
@@ -0,0 +1,17 @@
+# Full project: https://gitlab.com/pages/metalsmith
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules/
+
+ script:
+ - npm install -g metalsmith
+ - npm install
+ - make build
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml
new file mode 100644
index 00000000000..9f4cc0574d6
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml
@@ -0,0 +1,27 @@
+# Full project: https://gitlab.com/pages/middleman
+image: ruby:2.3
+
+cache:
+ paths:
+ - vendor
+
+test:
+ script:
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
+ except:
+ - master
+
+pages:
+ script:
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml
new file mode 100644
index 00000000000..b469b316ba5
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/nanoc
+image: ruby:2.3
+
+pages:
+ script:
+ - bundle install -j4
+ - nanoc
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml
new file mode 100644
index 00000000000..4762ec9acfd
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml
@@ -0,0 +1,15 @@
+# Full project: https://gitlab.com/pages/octopress
+image: ruby:2.3
+
+pages:
+ script:
+ - apt-get update -qq && apt-get install -qq nodejs
+ - bundle install -j4
+ - bundle exec rake generate
+ - mv public .public
+ - mv .public/octopress public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml
new file mode 100644
index 00000000000..c5f3154f587
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml
@@ -0,0 +1,10 @@
+# Full project: https://gitlab.com/pages/pelican
+image: python:2.7-alpine
+
+pages:
+ script:
+ - pip install -r requirements.txt
+ - pelican -s publishconf.py
+ artifacts:
+ paths:
+ - public/
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
new file mode 100644
index 00000000000..78f3e39949f
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -0,0 +1,30 @@
+# Official language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/ruby/tags/
+image: "ruby:2.3"
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ - gem install bundler # Bundler is not installed with the image
+ - bundle install -j $(nproc) # Install dependencies
+
+rubocop:
+ script:
+ - rubocop
+
+rspec:
+ script:
+ - rspec spec
+
+rails:
+ script:
+ - rake db:migrate
+ - rspec spec