From 046b28312704f3131e72dcd2dbdacc5264d4aa62 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 25 Aug 2015 18:42:46 -0700 Subject: [PATCH] Groundwork for merging CI into CE --- CHANGELOG-CI | 298 ++++++ Gemfile | 194 ++-- Gemfile.lock | 544 ++++++----- Procfile | 2 +- app/assets/images/ci/arch.jpg | Bin 0 -> 25222 bytes app/assets/images/ci/favicon.ico | Bin 0 -> 5430 bytes app/assets/images/ci/loader.gif | Bin 0 -> 4405 bytes app/assets/images/ci/no_avatar.png | Bin 0 -> 1337 bytes app/assets/images/ci/rails.png | Bin 0 -> 6646 bytes app/assets/images/ci/service_sample.png | Bin 0 -> 76024 bytes app/assets/javascripts/ci/Chart.min.js | 39 + .../javascripts/ci/application.js.coffee | 50 + app/assets/javascripts/ci/build.coffee | 41 + app/assets/javascripts/ci/pager.js.coffee | 42 + app/assets/javascripts/ci/projects.js.coffee | 6 + app/assets/stylesheets/ci/application.scss | 46 + app/assets/stylesheets/ci/generic/avatar.scss | 29 + .../stylesheets/ci/generic/buttons.scss | 7 + .../stylesheets/ci/generic/callout.scss | 45 + app/assets/stylesheets/ci/generic/common.scss | 189 ++++ app/assets/stylesheets/ci/generic/forms.scss | 28 + app/assets/stylesheets/ci/generic/tables.scss | 20 + .../stylesheets/ci/generic/typography.scss | 63 ++ app/assets/stylesheets/ci/generic/xterm.scss | 904 ++++++++++++++++++ app/assets/stylesheets/ci/main/fonts.scss | 2 + app/assets/stylesheets/ci/main/layout.scss | 18 + app/assets/stylesheets/ci/main/mixins.scss | 31 + app/assets/stylesheets/ci/main/variables.scss | 44 + .../stylesheets/ci/sections/builds.scss | 54 ++ app/assets/stylesheets/ci/sections/lint.scss | 8 + app/assets/stylesheets/ci/sections/login.scss | 13 + .../stylesheets/ci/sections/navbar.scss | 54 ++ .../stylesheets/ci/sections/projects.scss | 61 ++ .../stylesheets/ci/sections/runners.scss | 34 + app/assets/stylesheets/ci/sections/setup.scss | 11 + app/controllers/application_controller.rb | 11 +- .../ci/admin/application_controller.rb | 10 + .../admin/application_settings_controller.rb | 31 + app/controllers/ci/admin/builds_controller.rb | 12 + app/controllers/ci/admin/events_controller.rb | 9 + .../ci/admin/projects_controller.rb | 19 + .../ci/admin/runner_projects_controller.rb | 34 + .../ci/admin/runners_controller.rb | 69 ++ app/controllers/ci/application_controller.rb | 133 +++ app/controllers/ci/builds_controller.rb | 77 ++ app/controllers/ci/charts_controller.rb | 24 + app/controllers/ci/commits_controller.rb | 37 + app/controllers/ci/events_controller.rb | 21 + app/controllers/ci/helps_controller.rb | 16 + app/controllers/ci/lints_controller.rb | 26 + app/controllers/ci/projects_controller.rb | 136 +++ .../ci/runner_projects_controller.rb | 34 + app/controllers/ci/runners_controller.rb | 71 ++ app/controllers/ci/services_controller.rb | 59 ++ app/controllers/ci/triggers_controller.rb | 43 + .../ci/user_sessions_controller.rb | 65 ++ app/controllers/ci/variables_controller.rb | 33 + app/controllers/ci/web_hooks_controller.rb | 53 + .../oauth/applications_controller.rb | 2 +- .../authorized_applications_controller.rb | 2 +- .../projects/network_controller.rb | 2 +- app/controllers/projects/refs_controller.rb | 2 +- app/controllers/projects/wikis_controller.rb | 2 +- app/controllers/search_controller.rb | 2 +- app/helpers/appearances_helper.rb | 21 - app/helpers/application_helper.rb | 315 ------ app/helpers/application_settings_helper.rb | 59 -- app/helpers/auth_helper.rb | 50 - app/helpers/blob_helper.rb | 74 -- app/helpers/branches_helper.rb | 17 - app/helpers/broadcast_messages_helper.rb | 16 - app/helpers/ci/application_helper.rb | 140 +++ app/helpers/ci/builds_helper.rb | 41 + app/helpers/ci/commits_helper.rb | 26 + app/helpers/ci/gitlab_helper.rb | 36 + app/helpers/ci/icons_helper.rb | 11 + app/helpers/ci/projects_helper.rb | 36 + app/helpers/ci/routes_helper.rb | 29 + app/helpers/ci/runners_helper.rb | 22 + app/helpers/ci/triggers_helper.rb | 7 + app/helpers/ci/user_helper.rb | 15 + app/helpers/ci/user_sessions_helper.rb | 32 + app/helpers/commits_helper.rb | 183 ---- app/helpers/compare_helper.rb | 21 - app/helpers/dashboard_helper.rb | 9 - app/helpers/diff_helper.rb | 170 ---- app/helpers/emails_helper.rb | 57 -- app/helpers/events_helper.rb | 203 ---- app/helpers/explore_helper.rb | 17 - app/helpers/external_wiki_helper.rb | 11 - app/helpers/git_helper.rb | 5 - app/helpers/gitlab/appearances_helper.rb | 23 + app/helpers/gitlab/application_helper.rb | 317 ++++++ .../gitlab/application_settings_helper.rb | 61 ++ app/helpers/gitlab/auth_helper.rb | 52 + app/helpers/gitlab/blob_helper.rb | 76 ++ app/helpers/gitlab/branches_helper.rb | 19 + .../gitlab/broadcast_messages_helper.rb | 18 + app/helpers/gitlab/commits_helper.rb | 185 ++++ app/helpers/gitlab/compare_helper.rb | 23 + app/helpers/gitlab/dashboard_helper.rb | 11 + app/helpers/gitlab/diff_helper.rb | 172 ++++ app/helpers/gitlab/emails_helper.rb | 59 ++ app/helpers/gitlab/events_helper.rb | 205 ++++ app/helpers/gitlab/explore_helper.rb | 19 + app/helpers/gitlab/external_wiki_helper.rb | 13 + app/helpers/gitlab/git_helper.rb | 7 + app/helpers/gitlab/gitlab_markdown_helper.rb | 195 ++++ app/helpers/gitlab/gitlab_routing_helper.rb | 69 ++ app/helpers/gitlab/graph_helper.rb | 18 + app/helpers/gitlab/groups_helper.rb | 35 + app/helpers/gitlab/icons_helper.rb | 87 ++ app/helpers/gitlab/issues_helper.rb | 90 ++ app/helpers/gitlab/labels_helper.rb | 103 ++ app/helpers/gitlab/merge_requests_helper.rb | 76 ++ app/helpers/gitlab/milestones_helper.rb | 38 + app/helpers/gitlab/namespaces_helper.rb | 38 + app/helpers/gitlab/nav_helper.rb | 23 + app/helpers/gitlab/notes_helper.rb | 78 ++ app/helpers/gitlab/notifications_helper.rb | 17 + app/helpers/gitlab/page_layout_helper.rb | 28 + app/helpers/gitlab/preferences_helper.rb | 67 ++ app/helpers/gitlab/projects_helper.rb | 332 +++++++ app/helpers/gitlab/search_helper.rb | 114 +++ app/helpers/gitlab/selects_helper.rb | 47 + app/helpers/gitlab/snippets_helper.rb | 22 + app/helpers/gitlab/sorting_helper.rb | 98 ++ app/helpers/gitlab/submodule_helper.rb | 76 ++ app/helpers/gitlab/tab_helper.rb | 133 +++ app/helpers/gitlab/tags_helper.rb | 16 + app/helpers/gitlab/tree_helper.rb | 89 ++ app/helpers/gitlab/version_check_helper.rb | 9 + app/helpers/gitlab/visibility_level_helper.rb | 97 ++ app/helpers/gitlab/wiki_helper.rb | 26 + app/helpers/gitlab_markdown_helper.rb | 193 ---- app/helpers/gitlab_routing_helper.rb | 67 -- app/helpers/graph_helper.rb | 16 - app/helpers/groups_helper.rb | 33 - app/helpers/icons_helper.rb | 85 -- app/helpers/issues_helper.rb | 88 -- app/helpers/labels_helper.rb | 101 -- app/helpers/merge_requests_helper.rb | 74 -- app/helpers/milestones_helper.rb | 36 - app/helpers/namespaces_helper.rb | 36 - app/helpers/nav_helper.rb | 21 - app/helpers/notes_helper.rb | 76 -- app/helpers/notifications_helper.rb | 15 - app/helpers/page_layout_helper.rb | 26 - app/helpers/preferences_helper.rb | 65 -- app/helpers/projects_helper.rb | 330 ------- app/helpers/search_helper.rb | 112 --- app/helpers/selects_helper.rb | 45 - app/helpers/snippets_helper.rb | 20 - app/helpers/sorting_helper.rb | 96 -- app/helpers/submodule_helper.rb | 74 -- app/helpers/tab_helper.rb | 131 --- app/helpers/tags_helper.rb | 14 - app/helpers/tree_helper.rb | 88 -- app/helpers/version_check_helper.rb | 7 - app/helpers/visibility_level_helper.rb | 95 -- app/helpers/wiki_helper.rb | 24 - app/mailers/base_mailer.rb | 4 +- app/mailers/ci/emails/builds.rb | 17 + app/mailers/ci/notify.rb | 47 + app/mailers/notify.rb | 4 +- app/models/ci/application_setting.rb | 27 + app/models/ci/build.rb | 285 ++++++ app/models/ci/commit.rb | 267 ++++++ app/models/ci/event.rb | 27 + app/models/ci/network.rb | 122 +++ app/models/ci/project.rb | 221 +++++ app/models/ci/project_status.rb | 47 + app/models/ci/runner.rb | 80 ++ app/models/ci/runner_project.rb | 21 + app/models/ci/service.rb | 105 ++ app/models/ci/trigger.rb | 39 + app/models/ci/trigger_request.rb | 23 + app/models/ci/user.rb | 97 ++ app/models/ci/user_session.rb | 23 + app/models/ci/variable.rb | 25 + app/models/ci/web_hook.rb | 44 + app/models/project.rb | 4 +- .../project_services/ci/hip_chat_message.rb | 78 ++ .../project_services/ci/hip_chat_service.rb | 93 ++ .../project_services/ci/mail_service.rb | 84 ++ .../project_services/ci/slack_message.rb | 97 ++ .../project_services/ci/slack_service.rb | 81 ++ .../gitlab_issue_tracker_service.rb | 2 +- app/models/project_services/jira_service.rb | 2 +- app/services/ci/create_commit_service.rb | 50 + app/services/ci/create_project_service.rb | 35 + .../ci/create_trigger_request_service.rb | 17 + app/services/ci/event_service.rb | 31 + app/services/ci/image_for_build_service.rb | 31 + app/services/ci/register_build_service.rb | 40 + app/services/ci/test_hook_service.rb | 7 + app/services/ci/web_hook_service.rb | 36 + .../application_settings/_form.html.haml | 24 + .../admin/application_settings/show.html.haml | 3 + app/views/ci/admin/builds/_build.html.haml | 32 + app/views/ci/admin/builds/index.html.haml | 27 + app/views/ci/admin/events/index.html.haml | 17 + .../ci/admin/projects/_project.html.haml | 28 + app/views/ci/admin/projects/index.html.haml | 14 + .../ci/admin/runner_projects/index.html.haml | 57 ++ app/views/ci/admin/runners/_runner.html.haml | 48 + app/views/ci/admin/runners/index.html.haml | 51 + app/views/ci/admin/runners/show.html.haml | 118 +++ app/views/ci/admin/runners/update.js.haml | 2 + app/views/ci/builds/_build.html.haml | 45 + app/views/ci/builds/show.html.haml | 176 ++++ app/views/ci/charts/_build_times.haml | 21 + app/views/ci/charts/_builds.haml | 41 + app/views/ci/charts/_overall.haml | 21 + app/views/ci/charts/show.html.haml | 4 + app/views/ci/commits/_commit.html.haml | 32 + app/views/ci/commits/show.html.haml | 96 ++ app/views/ci/errors/show.haml | 2 + app/views/ci/events/index.html.haml | 19 + app/views/ci/helps/oauth2.html.haml | 20 + app/views/ci/helps/show.html.haml | 40 + app/views/ci/kaminari/_first_page.html.haml | 2 + app/views/ci/kaminari/_gap.html.haml | 2 + app/views/ci/kaminari/_last_page.html.haml | 2 + app/views/ci/kaminari/_next_page.html.haml | 2 + app/views/ci/kaminari/_page.html.haml | 2 + app/views/ci/kaminari/_paginator.html.haml | 11 + app/views/ci/kaminari/_prev_page.html.haml | 2 + app/views/ci/lints/_create.html.haml | 39 + app/views/ci/lints/create.js.haml | 2 + app/views/ci/lints/show.html.haml | 25 + .../ci/notify/build_fail_email.html.haml | 19 + app/views/ci/notify/build_fail_email.text.erb | 9 + .../ci/notify/build_success_email.html.haml | 20 + .../ci/notify/build_success_email.text.erb | 9 + app/views/ci/projects/_form.html.haml | 101 ++ app/views/ci/projects/_gl_projects.html.haml | 15 + app/views/ci/projects/_info.html.haml | 2 + app/views/ci/projects/_no_runners.html.haml | 8 + app/views/ci/projects/_project.html.haml | 22 + app/views/ci/projects/_public.html.haml | 21 + app/views/ci/projects/_search.html.haml | 18 + app/views/ci/projects/edit.html.haml | 21 + app/views/ci/projects/gitlab.html.haml | 35 + app/views/ci/projects/index.html.haml | 22 + app/views/ci/projects/show.html.haml | 59 ++ app/views/ci/runners/_runner.html.haml | 35 + .../ci/runners/_shared_runners.html.haml | 23 + .../ci/runners/_specific_runners.html.haml | 29 + app/views/ci/runners/edit.html.haml | 27 + app/views/ci/runners/index.html.haml | 25 + app/views/ci/runners/show.html.haml | 64 ++ app/views/ci/services/_form.html.haml | 57 ++ app/views/ci/services/edit.html.haml | 1 + app/views/ci/services/index.html.haml | 22 + app/views/ci/shared/_guide.html.haml | 15 + app/views/ci/shared/_no_runners.html.haml | 7 + app/views/ci/triggers/_trigger.html.haml | 14 + app/views/ci/triggers/index.html.haml | 67 ++ app/views/ci/user_sessions/new.html.haml | 8 + app/views/ci/user_sessions/show.html.haml | 15 + app/views/ci/variables/show.html.haml | 37 + app/views/ci/web_hooks/index.html.haml | 92 ++ app/views/layouts/ci/_head.html.haml | 11 + app/views/layouts/ci/_info.html.haml | 9 + app/views/layouts/ci/_nav.html.haml | 32 + app/views/layouts/ci/_nav_admin.html.haml | 28 + app/views/layouts/ci/_nav_project.html.haml | 40 + app/views/layouts/ci/admin.html.haml | 17 + app/views/layouts/ci/application.html.haml | 13 + app/views/layouts/ci/empty.html.haml | 13 + app/views/layouts/ci/notify.html.haml | 19 + app/views/layouts/ci/project.html.haml | 26 + app/workers/ci/hip_chat_notifier_worker.rb | 19 + app/workers/ci/slack_notifier_worker.rb | 10 + app/workers/ci/web_hook_worker.rb | 9 + bin/background_jobs | 2 +- bin/ci/upgrade.rb | 3 + config/environments/development.rb | 5 + config/gitlab_ci.yml | 19 + config/gitlab_ci.yml.example | 68 ++ config/gitlab_ci.yml.example.development | 19 + config/initializers/3_ci_settings.rb | 61 ++ config/initializers/3_grit_ext.rb | 5 - config/initializers/4_ci_app.rb | 10 + config/initializers/7_omniauth.rb | 28 - config/initializers/connection_fix.rb | 32 + config/initializers/cookies_serializer.rb | 3 + ..._url_options.rb => default_url_options.rb} | 2 +- config/initializers/rack_attack.rb.example | 14 +- .../{6_rack_profiler.rb => rack_profiler.rb} | 2 +- config/initializers/secret_token.rb | 24 + config/initializers/session_store.rb | 2 +- .../initializers/{4_sidekiq.rb => sidekiq.rb} | 0 config/initializers/static_files.rb | 2 +- config/locales/devise.en.yml | 7 +- config/routes.rb | 99 ++ config/schedule.rb | 8 + config/secrets.yml | 3 + config/secrets.yml.example | 12 + config/sidekiq.yml.example | 2 + .../migrate/20121004140911_create_projects.rb | 14 + db/ci/migrate/20121004165038_create_builds.rb | 15 + .../20121101091638_devise_create_users.rb | 46 + .../20121101121639_add_token_to_project.rb | 5 + .../20121106143042_add_ref_functionality.rb | 10 + ...0121108160657_add_gitlab_url_to_project.rb | 5 + .../20121108174237_add_started_at_to_build.rb | 5 + ...21115094430_increate_trace_colunm_limit.rb | 8 + .../20121115132252_add_tmp_file_to_build.rb | 5 + .../20121116144312_add_before_sha_to_build.rb | 5 + ...20121224092350_add_schedule_to_projects.rb | 6 + ...20130114153451_change_schedule_invertal.rb | 25 + ...130129121754_add_public_flag_to_project.rb | 5 + .../20130531112551_add_data_field_to_build.rb | 5 + ...31122131_remove_path_field_from_project.rb | 8 + .../migrate/20130531125905_create_runners.rb | 10 + .../20130531133603_add_runner_id_to_build.rb | 5 + .../20130603130920_remove_users_table.rb | 5 + ...130603144030_add_more_fields_to_project.rb | 5 + .../20130603144959_create_runner_projects.rb | 10 + ...161449_add_project_gitlab_id_to_project.rb | 5 + ...28142321_add_index_project_id_to_builds.rb | 5 + ...0130705171042_add_description_to_runner.rb | 5 + db/ci/migrate/20130710164015_add_db_index.rb | 7 + .../20130816201200_change_push_data_limit.rb | 5 + .../20130906175737_add_sessions_table.rb | 12 + ...23103430_add_allow_git_fetch_to_project.rb | 5 + ...dd_email_notification_fields_to_project.rb | 7 + .../20140130121538_rename_project_fields.rb | 5 + .../migrate/20140222210357_create_web_hook.rb | 9 + ...506091853_remove_public_key_from_runner.rb | 5 + ...140823225019_create_commits_from_builds.rb | 22 + ...0140909142245_add_skip_refs_to_projects.rb | 5 + .../20141001125939_add_coverage_parser.rb | 5 + .../20141001132129_add_coverage_to_build.rb | 5 + .../20141028162820_add_sha_index_to_build.rb | 6 + ...20141031114419_migrate_build_to_commits.rb | 21 + .../20141031141708_add_commit_indicies.rb | 9 + .../20141103135037_add_parallel_to_build.rb | 12 + .../20141103151359_add_commands_to_build.rb | 5 + .../20141103162726_add_job_id_to_build.rb | 5 + db/ci/migrate/20141104130024_migrate_jobs.rb | 12 + .../migrate/20141104153744_add_name_to_job.rb | 5 + ...41127153745_remove_scripts_from_project.rb | 5 + .../20141201153755_remove_invalid_build.rb | 5 + .../migrate/20141204133321_create_service.rb | 15 + .../20150111062026_add_filter_to_jobs.rb | 6 + ...on_migration.acts_as_taggable_on_engine.rb | 31 + ...ique_indices.acts_as_taggable_on_engine.rb | 20 + ...ache_to_tags.acts_as_taggable_on_engine.rb | 15 + ...ggable_index.acts_as_taggable_on_engine.rb | 10 + .../20150204001035_build_missing_services.rb | 21 + .../20150226001835_add_job_type_to_job.rb | 6 + ...150306131416_add_contacted_at_to_runner.rb | 5 + .../20150306135341_add_active_to_runner.rb | 5 + ...150310001733_rename_committer_to_pusher.rb | 5 + .../20150320001810_create_event_table.rb | 16 + ...4001123_add_settings_for_shared_runners.rb | 6 + .../20150324001227_migrate_shared_runners.rb | 11 + .../20150330001111_disable_shared_runners.rb | 8 + .../20150415142013_add_deleted_at_to_jobs.rb | 6 + .../20150417000045_cleanup_the_build_model.rb | 9 + .../20150504010150_migrate_url_to_path.rb | 11 + ...0150504010250_rename_gitlab_url_to_path.rb | 5 + ...0150508011360_add_info_fields_to_runner.rb | 9 + .../20150528011001_add_fields_to_builds.rb | 6 + .../20150528011012_move_job_name_to_build.rb | 10 + .../20150529012113_add_tag_to_commits.rb | 5 + .../20150601043220_add_yaml_to_projects.rb | 9 + .../20150601043231_migrate_jobs_to_yaml.rb | 97 ++ ...0602000240_change_default_build_timeout.rb | 9 + .../20150605002131_create_variables.rb | 11 + .../20150616001155_add_errors_to_commit.rb | 5 + .../20150630091815_add_options_to_build.rb | 5 + ...125244_add_encrypted_value_to_variables.rb | 7 + .../20150703125325_encrypt_variables.rb | 10 + ...50707134456_add_allow_failure_to_builds.rb | 5 + .../20150710113836_add_job_type_to_builds.rb | 5 + ...1_migrate_deploy_to_job_type_for_builds.rb | 6 + .../20150721204649_truncate_sessions.rb | 9 + ...50729145246_create_application_settings.rb | 10 + ...3142346_rename_job_type_to_stage_builds.rb | 9 + ...50806091503_add_committed_at_to_commits.rb | 6 + ...655_update_committed_at_with_created_at.rb | 5 + .../migrate/20150806102222_create_trigger.rb | 12 + .../20150806102457_add_trigger_to_builds.rb | 5 + .../20150806105404_create_trigger_request.rb | 9 + ...62227_add_commit_id_to_trigger_requests.rb | 8 + db/ci/schema.rb | 226 +++++ db/ci/seeds.rb | 0 db/migrate/20150826001931_add_ci_tables.rb | 190 ++++ db/migrate/limits_to_mysql.rb | 4 + db/schema.rb | 334 ++++++- doc/ci/README.md | 27 + doc/ci/api/README.md | 87 ++ doc/ci/api/builds.md | 41 + doc/ci/api/commits.md | 101 ++ doc/ci/api/forks.md | 23 + doc/ci/api/projects.md | 154 +++ doc/ci/api/runners.md | 77 ++ doc/ci/deployment/README.md | 98 ++ doc/ci/docker/README.md | 4 + doc/ci/docker/using_docker_build.md | 112 +++ doc/ci/docker/using_docker_images.md | 203 ++++ doc/ci/examples/README.md | 5 + ...and-deploy-python-application-to-heroku.md | 72 ++ ...t-and-deploy-ruby-application-to-heroku.md | 67 ++ doc/ci/examples/test-clojure-application.md | 35 + doc/ci/install/README.md | 276 ++++++ doc/ci/install/requirements.md | 61 ++ doc/ci/migration_to_omnibus/README.md | 29 + doc/ci/permissions/README.md | 24 + doc/ci/quick_start/README.md | 119 +++ doc/ci/quick_start/build_status.png | Bin 0 -> 62140 bytes doc/ci/quick_start/commit_status.png | Bin 0 -> 33492 bytes doc/ci/quick_start/new_commit.png | Bin 0 -> 47527 bytes doc/ci/quick_start/projects.png | Bin 0 -> 37014 bytes doc/ci/quick_start/runners.png | Bin 0 -> 123048 bytes doc/ci/quick_start/runners_activated.png | Bin 0 -> 60769 bytes doc/ci/raketasks/README.md | 3 + doc/ci/raketasks/backup_restore.md | 237 +++++ doc/ci/runners/README.md | 145 +++ doc/ci/runners/project_specific.png | Bin 0 -> 31408 bytes doc/ci/runners/shared_runner.png | Bin 0 -> 18366 bytes doc/ci/runners/shared_to_specific_admin.png | Bin 0 -> 5897 bytes doc/ci/update/3.0-to-3.1.md | 30 + doc/ci/update/3.1-to-3.2.md | 30 + doc/ci/update/3.2-to-4.0.md | 35 + doc/ci/update/4.0-to-4.1.md | 49 + doc/ci/update/4.1-to-4.2.md | 47 + doc/ci/update/4.2-to-4.3.md | 61 ++ doc/ci/update/4.3-to-5.0.md | 42 + doc/ci/update/5.0-to-5.1.md | 42 + doc/ci/update/5.1-to-5.2.md | 42 + doc/ci/update/5.2-to-5.3.md | 42 + doc/ci/update/5.3-to-5.4.md | 60 ++ doc/ci/update/5.4-to-7.8.md | 65 ++ doc/ci/update/7.10-to-7.11.md | 45 + doc/ci/update/7.11-to-7.12.md | 67 ++ doc/ci/update/7.12-to-7.13.md | 63 ++ doc/ci/update/7.8-to-7.9.md | 66 ++ doc/ci/update/7.9-to-7.10.md | 49 + doc/ci/update/README.md | 2 + doc/ci/update/patch_versions.md | 59 ++ doc/ci/variables/README.md | 95 ++ doc/ci/yaml/README.md | 204 ++++ lib/api/entities.rb | 4 +- lib/ci/ansi2html.rb | 224 +++++ lib/ci/api/api.rb | 37 + lib/ci/api/builds.rb | 53 + lib/ci/api/commits.rb | 66 ++ lib/ci/api/entities.rb | 44 + lib/ci/api/forks.rb | 40 + lib/ci/api/helpers.rb | 114 +++ lib/ci/api/projects.rb | 209 ++++ lib/ci/api/runners.rb | 69 ++ lib/ci/api/triggers.rb | 49 + lib/ci/assets/.gitkeep | 0 lib/ci/backup/builds.rb | 32 + lib/ci/backup/database.rb | 94 ++ lib/ci/backup/manager.rb | 158 +++ lib/ci/charts.rb | 71 ++ lib/ci/current_settings.rb | 22 + lib/ci/git.rb | 5 + lib/ci/gitlab_ci_yaml_processor.rb | 198 ++++ lib/ci/model.rb | 11 + lib/ci/scheduler.rb | 16 + lib/ci/static_model.rb | 49 + lib/ci/version_info.rb | 52 + .../markdown/commit_range_reference_filter.rb | 2 +- .../markdown/commit_reference_filter.rb | 2 +- lib/gitlab/markdown/label_reference_filter.rb | 2 +- .../merge_request_reference_filter.rb | 2 +- .../markdown/snippet_reference_filter.rb | 2 +- lib/gitlab/markdown/user_reference_filter.rb | 2 +- lib/gitlab/url_builder.rb | 4 +- lib/tasks/ci/.gitkeep | 0 lib/tasks/ci/backup.rake | 62 ++ lib/tasks/ci/cleanup.rake | 8 + lib/tasks/ci/schedule_builds.rake | 6 + lib/tasks/ci/setup.rake | 7 + lib/tasks/ci/sidekiq.rake | 13 + public/ci/build-canceled.svg | 1 + public/ci/build-failed.svg | 1 + public/ci/build-pending.svg | 1 + public/ci/build-running.svg | 1 + public/ci/build-success.svg | 1 + public/ci/build-unknown.svg | 1 + public/ci/favicon.ico | Bin 0 -> 5430 bytes scripts/ci/prepare_build.sh | 22 + .../ci/controllers/commits_controller_spec.rb | 27 + .../controllers/projects_controller_spec.rb | 108 +++ spec/ci/factories/builds.rb | 45 + spec/ci/factories/commits.rb | 75 ++ spec/ci/factories/events.rb | 24 + spec/ci/factories/projects.rb | 56 ++ spec/ci/factories/runner_projects.rb | 19 + spec/ci/factories/runners.rb | 38 + spec/ci/factories/trigger_requests.rb | 13 + spec/ci/factories/triggers.rb | 9 + spec/ci/factories/users.rb | 6 + spec/ci/factories/web_hook.rb | 6 + spec/ci/features/admin/builds_spec.rb | 71 ++ spec/ci/features/admin/events_spec.rb | 20 + spec/ci/features/admin/projects_spec.rb | 19 + spec/ci/features/admin/runners_spec.rb | 63 ++ spec/ci/features/builds_spec.rb | 57 ++ spec/ci/features/commits_spec.rb | 66 ++ spec/ci/features/events_spec.rb | 20 + spec/ci/features/lint_spec.rb | 28 + spec/ci/features/projects_spec.rb | 57 ++ spec/ci/features/runners_spec.rb | 98 ++ spec/ci/features/triggers_spec.rb | 26 + spec/ci/features/variables_spec.rb | 26 + spec/ci/helpers/application_helper_spec.rb | 37 + spec/ci/helpers/runners_helper_spec.rb | 18 + spec/ci/helpers/user_helper_spec.rb | 49 + spec/ci/helpers/user_sessions_helper_spec.rb | 69 ++ spec/ci/lib/ansi2html_spec.rb | 133 +++ spec/ci/lib/charts_spec.rb | 17 + spec/ci/lib/gitlab_ci_yaml_processor_spec.rb | 311 ++++++ spec/ci/lib/upgrader_spec.rb | 39 + spec/ci/mailers/notify_spec.rb | 36 + spec/ci/models/build_spec.rb | 350 +++++++ spec/ci/models/commit_spec.rb | 264 +++++ spec/ci/models/mail_service_spec.rb | 184 ++++ spec/ci/models/network_spec.rb | 54 ++ .../project_services/hip_chat_message_spec.rb | 74 ++ .../project_services/hip_chat_service_spec.rb | 75 ++ .../project_services/slack_message_spec.rb | 84 ++ .../project_services/slack_service_spec.rb | 58 ++ spec/ci/models/project_spec.rb | 185 ++++ spec/ci/models/runner_project_spec.rb | 16 + spec/ci/models/runner_spec.rb | 70 ++ spec/ci/models/service_spec.rb | 49 + spec/ci/models/trigger_spec.rb | 17 + spec/ci/models/user_spec.rb | 100 ++ spec/ci/models/variable_spec.rb | 44 + spec/ci/models/web_hook_spec.rb | 64 ++ spec/ci/requests/api/builds_spec.rb | 115 +++ spec/ci/requests/api/commits_spec.rb | 65 ++ spec/ci/requests/api/forks_spec.rb | 60 ++ spec/ci/requests/api/projects_spec.rb | 251 +++++ spec/ci/requests/api/runners_spec.rb | 83 ++ spec/ci/requests/api/triggers_spec.rb | 78 ++ spec/ci/requests/builds_spec.rb | 18 + spec/ci/requests/commits_spec.rb | 17 + .../ci/services/create_commit_service_spec.rb | 130 +++ .../services/create_project_service_spec.rb | 40 + .../create_trigger_request_service_spec.rb | 52 + spec/ci/services/event_service_spec.rb | 34 + .../services/image_for_build_service_spec.rb | 46 + .../services/register_build_service_spec.rb | 89 ++ spec/ci/services/web_hook_service_spec.rb | 36 + spec/ci/six.tar.gz | Bin 0 -> 61937 bytes spec/ci/spec_helper.rb | 60 ++ spec/ci/support/api_helpers.rb | 35 + spec/ci/support/db_cleaner.rb | 39 + spec/ci/support/gitlab_stubs/gitlab_ci.yml | 63 ++ spec/ci/support/gitlab_stubs/project_8.json | 45 + .../support/gitlab_stubs/project_8_hooks.json | 1 + spec/ci/support/gitlab_stubs/projects.json | 1 + spec/ci/support/gitlab_stubs/raw_project.yml | 36 + spec/ci/support/gitlab_stubs/session.json | 20 + spec/ci/support/gitlab_stubs/user.json | 20 + spec/ci/support/login_helpers.rb | 22 + spec/ci/support/monkey_patches/oauth2.rb | 7 + spec/ci/support/setup_builds_storage.rb | 16 + spec/ci/support/stub_gitlab_calls.rb | 77 ++ spec/ci/support/stub_gitlab_data.rb | 5 + spec/lib/extracts_path_spec.rb | 2 +- spec/support/filter_spec_helper.rb | 2 +- 573 files changed, 23499 insertions(+), 3644 deletions(-) create mode 100644 CHANGELOG-CI create mode 100644 app/assets/images/ci/arch.jpg create mode 100644 app/assets/images/ci/favicon.ico create mode 100644 app/assets/images/ci/loader.gif create mode 100644 app/assets/images/ci/no_avatar.png create mode 100644 app/assets/images/ci/rails.png create mode 100644 app/assets/images/ci/service_sample.png create mode 100644 app/assets/javascripts/ci/Chart.min.js create mode 100644 app/assets/javascripts/ci/application.js.coffee create mode 100644 app/assets/javascripts/ci/build.coffee create mode 100644 app/assets/javascripts/ci/pager.js.coffee create mode 100644 app/assets/javascripts/ci/projects.js.coffee create mode 100644 app/assets/stylesheets/ci/application.scss create mode 100644 app/assets/stylesheets/ci/generic/avatar.scss create mode 100644 app/assets/stylesheets/ci/generic/buttons.scss create mode 100644 app/assets/stylesheets/ci/generic/callout.scss create mode 100644 app/assets/stylesheets/ci/generic/common.scss create mode 100644 app/assets/stylesheets/ci/generic/forms.scss create mode 100644 app/assets/stylesheets/ci/generic/tables.scss create mode 100644 app/assets/stylesheets/ci/generic/typography.scss create mode 100644 app/assets/stylesheets/ci/generic/xterm.scss create mode 100644 app/assets/stylesheets/ci/main/fonts.scss create mode 100644 app/assets/stylesheets/ci/main/layout.scss create mode 100644 app/assets/stylesheets/ci/main/mixins.scss create mode 100644 app/assets/stylesheets/ci/main/variables.scss create mode 100644 app/assets/stylesheets/ci/sections/builds.scss create mode 100644 app/assets/stylesheets/ci/sections/lint.scss create mode 100644 app/assets/stylesheets/ci/sections/login.scss create mode 100644 app/assets/stylesheets/ci/sections/navbar.scss create mode 100644 app/assets/stylesheets/ci/sections/projects.scss create mode 100644 app/assets/stylesheets/ci/sections/runners.scss create mode 100644 app/assets/stylesheets/ci/sections/setup.scss create mode 100644 app/controllers/ci/admin/application_controller.rb create mode 100644 app/controllers/ci/admin/application_settings_controller.rb create mode 100644 app/controllers/ci/admin/builds_controller.rb create mode 100644 app/controllers/ci/admin/events_controller.rb create mode 100644 app/controllers/ci/admin/projects_controller.rb create mode 100644 app/controllers/ci/admin/runner_projects_controller.rb create mode 100644 app/controllers/ci/admin/runners_controller.rb create mode 100644 app/controllers/ci/application_controller.rb create mode 100644 app/controllers/ci/builds_controller.rb create mode 100644 app/controllers/ci/charts_controller.rb create mode 100644 app/controllers/ci/commits_controller.rb create mode 100644 app/controllers/ci/events_controller.rb create mode 100644 app/controllers/ci/helps_controller.rb create mode 100644 app/controllers/ci/lints_controller.rb create mode 100644 app/controllers/ci/projects_controller.rb create mode 100644 app/controllers/ci/runner_projects_controller.rb create mode 100644 app/controllers/ci/runners_controller.rb create mode 100644 app/controllers/ci/services_controller.rb create mode 100644 app/controllers/ci/triggers_controller.rb create mode 100644 app/controllers/ci/user_sessions_controller.rb create mode 100644 app/controllers/ci/variables_controller.rb create mode 100644 app/controllers/ci/web_hooks_controller.rb delete mode 100644 app/helpers/appearances_helper.rb delete mode 100644 app/helpers/application_helper.rb delete mode 100644 app/helpers/application_settings_helper.rb delete mode 100644 app/helpers/auth_helper.rb delete mode 100644 app/helpers/blob_helper.rb delete mode 100644 app/helpers/branches_helper.rb delete mode 100644 app/helpers/broadcast_messages_helper.rb create mode 100644 app/helpers/ci/application_helper.rb create mode 100644 app/helpers/ci/builds_helper.rb create mode 100644 app/helpers/ci/commits_helper.rb create mode 100644 app/helpers/ci/gitlab_helper.rb create mode 100644 app/helpers/ci/icons_helper.rb create mode 100644 app/helpers/ci/projects_helper.rb create mode 100644 app/helpers/ci/routes_helper.rb create mode 100644 app/helpers/ci/runners_helper.rb create mode 100644 app/helpers/ci/triggers_helper.rb create mode 100644 app/helpers/ci/user_helper.rb create mode 100644 app/helpers/ci/user_sessions_helper.rb delete mode 100644 app/helpers/commits_helper.rb delete mode 100644 app/helpers/compare_helper.rb delete mode 100644 app/helpers/dashboard_helper.rb delete mode 100644 app/helpers/diff_helper.rb delete mode 100644 app/helpers/emails_helper.rb delete mode 100644 app/helpers/events_helper.rb delete mode 100644 app/helpers/explore_helper.rb delete mode 100644 app/helpers/external_wiki_helper.rb delete mode 100644 app/helpers/git_helper.rb create mode 100644 app/helpers/gitlab/appearances_helper.rb create mode 100644 app/helpers/gitlab/application_helper.rb create mode 100644 app/helpers/gitlab/application_settings_helper.rb create mode 100644 app/helpers/gitlab/auth_helper.rb create mode 100644 app/helpers/gitlab/blob_helper.rb create mode 100644 app/helpers/gitlab/branches_helper.rb create mode 100644 app/helpers/gitlab/broadcast_messages_helper.rb create mode 100644 app/helpers/gitlab/commits_helper.rb create mode 100644 app/helpers/gitlab/compare_helper.rb create mode 100644 app/helpers/gitlab/dashboard_helper.rb create mode 100644 app/helpers/gitlab/diff_helper.rb create mode 100644 app/helpers/gitlab/emails_helper.rb create mode 100644 app/helpers/gitlab/events_helper.rb create mode 100644 app/helpers/gitlab/explore_helper.rb create mode 100644 app/helpers/gitlab/external_wiki_helper.rb create mode 100644 app/helpers/gitlab/git_helper.rb create mode 100644 app/helpers/gitlab/gitlab_markdown_helper.rb create mode 100644 app/helpers/gitlab/gitlab_routing_helper.rb create mode 100644 app/helpers/gitlab/graph_helper.rb create mode 100644 app/helpers/gitlab/groups_helper.rb create mode 100644 app/helpers/gitlab/icons_helper.rb create mode 100644 app/helpers/gitlab/issues_helper.rb create mode 100644 app/helpers/gitlab/labels_helper.rb create mode 100644 app/helpers/gitlab/merge_requests_helper.rb create mode 100644 app/helpers/gitlab/milestones_helper.rb create mode 100644 app/helpers/gitlab/namespaces_helper.rb create mode 100644 app/helpers/gitlab/nav_helper.rb create mode 100644 app/helpers/gitlab/notes_helper.rb create mode 100644 app/helpers/gitlab/notifications_helper.rb create mode 100644 app/helpers/gitlab/page_layout_helper.rb create mode 100644 app/helpers/gitlab/preferences_helper.rb create mode 100644 app/helpers/gitlab/projects_helper.rb create mode 100644 app/helpers/gitlab/search_helper.rb create mode 100644 app/helpers/gitlab/selects_helper.rb create mode 100644 app/helpers/gitlab/snippets_helper.rb create mode 100644 app/helpers/gitlab/sorting_helper.rb create mode 100644 app/helpers/gitlab/submodule_helper.rb create mode 100644 app/helpers/gitlab/tab_helper.rb create mode 100644 app/helpers/gitlab/tags_helper.rb create mode 100644 app/helpers/gitlab/tree_helper.rb create mode 100644 app/helpers/gitlab/version_check_helper.rb create mode 100644 app/helpers/gitlab/visibility_level_helper.rb create mode 100644 app/helpers/gitlab/wiki_helper.rb delete mode 100644 app/helpers/gitlab_markdown_helper.rb delete mode 100644 app/helpers/gitlab_routing_helper.rb delete mode 100644 app/helpers/graph_helper.rb delete mode 100644 app/helpers/groups_helper.rb delete mode 100644 app/helpers/icons_helper.rb delete mode 100644 app/helpers/issues_helper.rb delete mode 100644 app/helpers/labels_helper.rb delete mode 100644 app/helpers/merge_requests_helper.rb delete mode 100644 app/helpers/milestones_helper.rb delete mode 100644 app/helpers/namespaces_helper.rb delete mode 100644 app/helpers/nav_helper.rb delete mode 100644 app/helpers/notes_helper.rb delete mode 100644 app/helpers/notifications_helper.rb delete mode 100644 app/helpers/page_layout_helper.rb delete mode 100644 app/helpers/preferences_helper.rb delete mode 100644 app/helpers/projects_helper.rb delete mode 100644 app/helpers/search_helper.rb delete mode 100644 app/helpers/selects_helper.rb delete mode 100644 app/helpers/snippets_helper.rb delete mode 100644 app/helpers/sorting_helper.rb delete mode 100644 app/helpers/submodule_helper.rb delete mode 100644 app/helpers/tab_helper.rb delete mode 100644 app/helpers/tags_helper.rb delete mode 100644 app/helpers/tree_helper.rb delete mode 100644 app/helpers/version_check_helper.rb delete mode 100644 app/helpers/visibility_level_helper.rb delete mode 100644 app/helpers/wiki_helper.rb create mode 100644 app/mailers/ci/emails/builds.rb create mode 100644 app/mailers/ci/notify.rb create mode 100644 app/models/ci/application_setting.rb create mode 100644 app/models/ci/build.rb create mode 100644 app/models/ci/commit.rb create mode 100644 app/models/ci/event.rb create mode 100644 app/models/ci/network.rb create mode 100644 app/models/ci/project.rb create mode 100644 app/models/ci/project_status.rb create mode 100644 app/models/ci/runner.rb create mode 100644 app/models/ci/runner_project.rb create mode 100644 app/models/ci/service.rb create mode 100644 app/models/ci/trigger.rb create mode 100644 app/models/ci/trigger_request.rb create mode 100644 app/models/ci/user.rb create mode 100644 app/models/ci/user_session.rb create mode 100644 app/models/ci/variable.rb create mode 100644 app/models/ci/web_hook.rb create mode 100644 app/models/project_services/ci/hip_chat_message.rb create mode 100644 app/models/project_services/ci/hip_chat_service.rb create mode 100644 app/models/project_services/ci/mail_service.rb create mode 100644 app/models/project_services/ci/slack_message.rb create mode 100644 app/models/project_services/ci/slack_service.rb create mode 100644 app/services/ci/create_commit_service.rb create mode 100644 app/services/ci/create_project_service.rb create mode 100644 app/services/ci/create_trigger_request_service.rb create mode 100644 app/services/ci/event_service.rb create mode 100644 app/services/ci/image_for_build_service.rb create mode 100644 app/services/ci/register_build_service.rb create mode 100644 app/services/ci/test_hook_service.rb create mode 100644 app/services/ci/web_hook_service.rb create mode 100644 app/views/ci/admin/application_settings/_form.html.haml create mode 100644 app/views/ci/admin/application_settings/show.html.haml create mode 100644 app/views/ci/admin/builds/_build.html.haml create mode 100644 app/views/ci/admin/builds/index.html.haml create mode 100644 app/views/ci/admin/events/index.html.haml create mode 100644 app/views/ci/admin/projects/_project.html.haml create mode 100644 app/views/ci/admin/projects/index.html.haml create mode 100644 app/views/ci/admin/runner_projects/index.html.haml create mode 100644 app/views/ci/admin/runners/_runner.html.haml create mode 100644 app/views/ci/admin/runners/index.html.haml create mode 100644 app/views/ci/admin/runners/show.html.haml create mode 100644 app/views/ci/admin/runners/update.js.haml create mode 100644 app/views/ci/builds/_build.html.haml create mode 100644 app/views/ci/builds/show.html.haml create mode 100644 app/views/ci/charts/_build_times.haml create mode 100644 app/views/ci/charts/_builds.haml create mode 100644 app/views/ci/charts/_overall.haml create mode 100644 app/views/ci/charts/show.html.haml create mode 100644 app/views/ci/commits/_commit.html.haml create mode 100644 app/views/ci/commits/show.html.haml create mode 100644 app/views/ci/errors/show.haml create mode 100644 app/views/ci/events/index.html.haml create mode 100644 app/views/ci/helps/oauth2.html.haml create mode 100644 app/views/ci/helps/show.html.haml create mode 100644 app/views/ci/kaminari/_first_page.html.haml create mode 100644 app/views/ci/kaminari/_gap.html.haml create mode 100644 app/views/ci/kaminari/_last_page.html.haml create mode 100644 app/views/ci/kaminari/_next_page.html.haml create mode 100644 app/views/ci/kaminari/_page.html.haml create mode 100644 app/views/ci/kaminari/_paginator.html.haml create mode 100644 app/views/ci/kaminari/_prev_page.html.haml create mode 100644 app/views/ci/lints/_create.html.haml create mode 100644 app/views/ci/lints/create.js.haml create mode 100644 app/views/ci/lints/show.html.haml create mode 100644 app/views/ci/notify/build_fail_email.html.haml create mode 100644 app/views/ci/notify/build_fail_email.text.erb create mode 100644 app/views/ci/notify/build_success_email.html.haml create mode 100644 app/views/ci/notify/build_success_email.text.erb create mode 100644 app/views/ci/projects/_form.html.haml create mode 100644 app/views/ci/projects/_gl_projects.html.haml create mode 100644 app/views/ci/projects/_info.html.haml create mode 100644 app/views/ci/projects/_no_runners.html.haml create mode 100644 app/views/ci/projects/_project.html.haml create mode 100644 app/views/ci/projects/_public.html.haml create mode 100644 app/views/ci/projects/_search.html.haml create mode 100644 app/views/ci/projects/edit.html.haml create mode 100644 app/views/ci/projects/gitlab.html.haml create mode 100644 app/views/ci/projects/index.html.haml create mode 100644 app/views/ci/projects/show.html.haml create mode 100644 app/views/ci/runners/_runner.html.haml create mode 100644 app/views/ci/runners/_shared_runners.html.haml create mode 100644 app/views/ci/runners/_specific_runners.html.haml create mode 100644 app/views/ci/runners/edit.html.haml create mode 100644 app/views/ci/runners/index.html.haml create mode 100644 app/views/ci/runners/show.html.haml create mode 100644 app/views/ci/services/_form.html.haml create mode 100644 app/views/ci/services/edit.html.haml create mode 100644 app/views/ci/services/index.html.haml create mode 100644 app/views/ci/shared/_guide.html.haml create mode 100644 app/views/ci/shared/_no_runners.html.haml create mode 100644 app/views/ci/triggers/_trigger.html.haml create mode 100644 app/views/ci/triggers/index.html.haml create mode 100644 app/views/ci/user_sessions/new.html.haml create mode 100644 app/views/ci/user_sessions/show.html.haml create mode 100644 app/views/ci/variables/show.html.haml create mode 100644 app/views/ci/web_hooks/index.html.haml create mode 100644 app/views/layouts/ci/_head.html.haml create mode 100644 app/views/layouts/ci/_info.html.haml create mode 100644 app/views/layouts/ci/_nav.html.haml create mode 100644 app/views/layouts/ci/_nav_admin.html.haml create mode 100644 app/views/layouts/ci/_nav_project.html.haml create mode 100644 app/views/layouts/ci/admin.html.haml create mode 100644 app/views/layouts/ci/application.html.haml create mode 100644 app/views/layouts/ci/empty.html.haml create mode 100644 app/views/layouts/ci/notify.html.haml create mode 100644 app/views/layouts/ci/project.html.haml create mode 100644 app/workers/ci/hip_chat_notifier_worker.rb create mode 100644 app/workers/ci/slack_notifier_worker.rb create mode 100644 app/workers/ci/web_hook_worker.rb create mode 100644 bin/ci/upgrade.rb create mode 100644 config/gitlab_ci.yml create mode 100644 config/gitlab_ci.yml.example create mode 100644 config/gitlab_ci.yml.example.development create mode 100644 config/initializers/3_ci_settings.rb delete mode 100644 config/initializers/3_grit_ext.rb create mode 100644 config/initializers/4_ci_app.rb delete mode 100644 config/initializers/7_omniauth.rb create mode 100644 config/initializers/connection_fix.rb create mode 100644 config/initializers/cookies_serializer.rb rename config/initializers/{8_default_url_options.rb => default_url_options.rb} (80%) rename config/initializers/{6_rack_profiler.rb => rack_profiler.rb} (81%) rename config/initializers/{4_sidekiq.rb => sidekiq.rb} (100%) create mode 100644 config/schedule.rb create mode 100644 config/secrets.yml create mode 100644 config/secrets.yml.example create mode 100644 config/sidekiq.yml.example create mode 100644 db/ci/migrate/20121004140911_create_projects.rb create mode 100644 db/ci/migrate/20121004165038_create_builds.rb create mode 100644 db/ci/migrate/20121101091638_devise_create_users.rb create mode 100644 db/ci/migrate/20121101121639_add_token_to_project.rb create mode 100644 db/ci/migrate/20121106143042_add_ref_functionality.rb create mode 100644 db/ci/migrate/20121108160657_add_gitlab_url_to_project.rb create mode 100644 db/ci/migrate/20121108174237_add_started_at_to_build.rb create mode 100644 db/ci/migrate/20121115094430_increate_trace_colunm_limit.rb create mode 100644 db/ci/migrate/20121115132252_add_tmp_file_to_build.rb create mode 100644 db/ci/migrate/20121116144312_add_before_sha_to_build.rb create mode 100644 db/ci/migrate/20121224092350_add_schedule_to_projects.rb create mode 100644 db/ci/migrate/20130114153451_change_schedule_invertal.rb create mode 100644 db/ci/migrate/20130129121754_add_public_flag_to_project.rb create mode 100644 db/ci/migrate/20130531112551_add_data_field_to_build.rb create mode 100644 db/ci/migrate/20130531122131_remove_path_field_from_project.rb create mode 100644 db/ci/migrate/20130531125905_create_runners.rb create mode 100644 db/ci/migrate/20130531133603_add_runner_id_to_build.rb create mode 100644 db/ci/migrate/20130603130920_remove_users_table.rb create mode 100644 db/ci/migrate/20130603144030_add_more_fields_to_project.rb create mode 100644 db/ci/migrate/20130603144959_create_runner_projects.rb create mode 100644 db/ci/migrate/20130603161449_add_project_gitlab_id_to_project.rb create mode 100644 db/ci/migrate/20130628142321_add_index_project_id_to_builds.rb create mode 100644 db/ci/migrate/20130705171042_add_description_to_runner.rb create mode 100644 db/ci/migrate/20130710164015_add_db_index.rb create mode 100644 db/ci/migrate/20130816201200_change_push_data_limit.rb create mode 100644 db/ci/migrate/20130906175737_add_sessions_table.rb create mode 100644 db/ci/migrate/20131023103430_add_allow_git_fetch_to_project.rb create mode 100644 db/ci/migrate/20131120155545_add_email_notification_fields_to_project.rb create mode 100644 db/ci/migrate/20140130121538_rename_project_fields.rb create mode 100644 db/ci/migrate/20140222210357_create_web_hook.rb create mode 100644 db/ci/migrate/20140506091853_remove_public_key_from_runner.rb create mode 100644 db/ci/migrate/20140823225019_create_commits_from_builds.rb create mode 100644 db/ci/migrate/20140909142245_add_skip_refs_to_projects.rb create mode 100644 db/ci/migrate/20141001125939_add_coverage_parser.rb create mode 100644 db/ci/migrate/20141001132129_add_coverage_to_build.rb create mode 100644 db/ci/migrate/20141028162820_add_sha_index_to_build.rb create mode 100644 db/ci/migrate/20141031114419_migrate_build_to_commits.rb create mode 100644 db/ci/migrate/20141031141708_add_commit_indicies.rb create mode 100644 db/ci/migrate/20141103135037_add_parallel_to_build.rb create mode 100644 db/ci/migrate/20141103151359_add_commands_to_build.rb create mode 100644 db/ci/migrate/20141103162726_add_job_id_to_build.rb create mode 100644 db/ci/migrate/20141104130024_migrate_jobs.rb create mode 100644 db/ci/migrate/20141104153744_add_name_to_job.rb create mode 100644 db/ci/migrate/20141127153745_remove_scripts_from_project.rb create mode 100644 db/ci/migrate/20141201153755_remove_invalid_build.rb create mode 100644 db/ci/migrate/20141204133321_create_service.rb create mode 100644 db/ci/migrate/20150111062026_add_filter_to_jobs.rb create mode 100644 db/ci/migrate/20150113001832_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb create mode 100644 db/ci/migrate/20150113001833_add_missing_unique_indices.acts_as_taggable_on_engine.rb create mode 100644 db/ci/migrate/20150113001834_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb create mode 100644 db/ci/migrate/20150113001835_add_missing_taggable_index.acts_as_taggable_on_engine.rb create mode 100644 db/ci/migrate/20150204001035_build_missing_services.rb create mode 100644 db/ci/migrate/20150226001835_add_job_type_to_job.rb create mode 100644 db/ci/migrate/20150306131416_add_contacted_at_to_runner.rb create mode 100644 db/ci/migrate/20150306135341_add_active_to_runner.rb create mode 100644 db/ci/migrate/20150310001733_rename_committer_to_pusher.rb create mode 100644 db/ci/migrate/20150320001810_create_event_table.rb create mode 100644 db/ci/migrate/20150324001123_add_settings_for_shared_runners.rb create mode 100644 db/ci/migrate/20150324001227_migrate_shared_runners.rb create mode 100644 db/ci/migrate/20150330001111_disable_shared_runners.rb create mode 100644 db/ci/migrate/20150415142013_add_deleted_at_to_jobs.rb create mode 100644 db/ci/migrate/20150417000045_cleanup_the_build_model.rb create mode 100644 db/ci/migrate/20150504010150_migrate_url_to_path.rb create mode 100644 db/ci/migrate/20150504010250_rename_gitlab_url_to_path.rb create mode 100644 db/ci/migrate/20150508011360_add_info_fields_to_runner.rb create mode 100644 db/ci/migrate/20150528011001_add_fields_to_builds.rb create mode 100644 db/ci/migrate/20150528011012_move_job_name_to_build.rb create mode 100644 db/ci/migrate/20150529012113_add_tag_to_commits.rb create mode 100644 db/ci/migrate/20150601043220_add_yaml_to_projects.rb create mode 100644 db/ci/migrate/20150601043231_migrate_jobs_to_yaml.rb create mode 100644 db/ci/migrate/20150602000240_change_default_build_timeout.rb create mode 100644 db/ci/migrate/20150605002131_create_variables.rb create mode 100644 db/ci/migrate/20150616001155_add_errors_to_commit.rb create mode 100644 db/ci/migrate/20150630091815_add_options_to_build.rb create mode 100644 db/ci/migrate/20150703125244_add_encrypted_value_to_variables.rb create mode 100644 db/ci/migrate/20150703125325_encrypt_variables.rb create mode 100644 db/ci/migrate/20150707134456_add_allow_failure_to_builds.rb create mode 100644 db/ci/migrate/20150710113836_add_job_type_to_builds.rb create mode 100644 db/ci/migrate/20150710113851_migrate_deploy_to_job_type_for_builds.rb create mode 100644 db/ci/migrate/20150721204649_truncate_sessions.rb create mode 100644 db/ci/migrate/20150729145246_create_application_settings.rb create mode 100644 db/ci/migrate/20150803142346_rename_job_type_to_stage_builds.rb create mode 100644 db/ci/migrate/20150806091503_add_committed_at_to_commits.rb create mode 100644 db/ci/migrate/20150806091655_update_committed_at_with_created_at.rb create mode 100644 db/ci/migrate/20150806102222_create_trigger.rb create mode 100644 db/ci/migrate/20150806102457_add_trigger_to_builds.rb create mode 100644 db/ci/migrate/20150806105404_create_trigger_request.rb create mode 100644 db/ci/migrate/20150819162227_add_commit_id_to_trigger_requests.rb create mode 100644 db/ci/schema.rb create mode 100644 db/ci/seeds.rb create mode 100644 db/migrate/20150826001931_add_ci_tables.rb create mode 100644 doc/ci/README.md create mode 100644 doc/ci/api/README.md create mode 100644 doc/ci/api/builds.md create mode 100644 doc/ci/api/commits.md create mode 100644 doc/ci/api/forks.md create mode 100644 doc/ci/api/projects.md create mode 100644 doc/ci/api/runners.md create mode 100644 doc/ci/deployment/README.md create mode 100644 doc/ci/docker/README.md create mode 100644 doc/ci/docker/using_docker_build.md create mode 100644 doc/ci/docker/using_docker_images.md create mode 100644 doc/ci/examples/README.md create mode 100644 doc/ci/examples/test-and-deploy-python-application-to-heroku.md create mode 100644 doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md create mode 100644 doc/ci/examples/test-clojure-application.md create mode 100644 doc/ci/install/README.md create mode 100644 doc/ci/install/requirements.md create mode 100644 doc/ci/migration_to_omnibus/README.md create mode 100644 doc/ci/permissions/README.md create mode 100644 doc/ci/quick_start/README.md create mode 100644 doc/ci/quick_start/build_status.png create mode 100644 doc/ci/quick_start/commit_status.png create mode 100644 doc/ci/quick_start/new_commit.png create mode 100644 doc/ci/quick_start/projects.png create mode 100644 doc/ci/quick_start/runners.png create mode 100644 doc/ci/quick_start/runners_activated.png create mode 100644 doc/ci/raketasks/README.md create mode 100644 doc/ci/raketasks/backup_restore.md create mode 100644 doc/ci/runners/README.md create mode 100644 doc/ci/runners/project_specific.png create mode 100644 doc/ci/runners/shared_runner.png create mode 100644 doc/ci/runners/shared_to_specific_admin.png create mode 100644 doc/ci/update/3.0-to-3.1.md create mode 100644 doc/ci/update/3.1-to-3.2.md create mode 100644 doc/ci/update/3.2-to-4.0.md create mode 100644 doc/ci/update/4.0-to-4.1.md create mode 100644 doc/ci/update/4.1-to-4.2.md create mode 100644 doc/ci/update/4.2-to-4.3.md create mode 100644 doc/ci/update/4.3-to-5.0.md create mode 100644 doc/ci/update/5.0-to-5.1.md create mode 100644 doc/ci/update/5.1-to-5.2.md create mode 100644 doc/ci/update/5.2-to-5.3.md create mode 100644 doc/ci/update/5.3-to-5.4.md create mode 100644 doc/ci/update/5.4-to-7.8.md create mode 100644 doc/ci/update/7.10-to-7.11.md create mode 100644 doc/ci/update/7.11-to-7.12.md create mode 100644 doc/ci/update/7.12-to-7.13.md create mode 100644 doc/ci/update/7.8-to-7.9.md create mode 100644 doc/ci/update/7.9-to-7.10.md create mode 100644 doc/ci/update/README.md create mode 100644 doc/ci/update/patch_versions.md create mode 100644 doc/ci/variables/README.md create mode 100644 doc/ci/yaml/README.md create mode 100644 lib/ci/ansi2html.rb create mode 100644 lib/ci/api/api.rb create mode 100644 lib/ci/api/builds.rb create mode 100644 lib/ci/api/commits.rb create mode 100644 lib/ci/api/entities.rb create mode 100644 lib/ci/api/forks.rb create mode 100644 lib/ci/api/helpers.rb create mode 100644 lib/ci/api/projects.rb create mode 100644 lib/ci/api/runners.rb create mode 100644 lib/ci/api/triggers.rb create mode 100644 lib/ci/assets/.gitkeep create mode 100644 lib/ci/backup/builds.rb create mode 100644 lib/ci/backup/database.rb create mode 100644 lib/ci/backup/manager.rb create mode 100644 lib/ci/charts.rb create mode 100644 lib/ci/current_settings.rb create mode 100644 lib/ci/git.rb create mode 100644 lib/ci/gitlab_ci_yaml_processor.rb create mode 100644 lib/ci/model.rb create mode 100644 lib/ci/scheduler.rb create mode 100644 lib/ci/static_model.rb create mode 100644 lib/ci/version_info.rb create mode 100644 lib/tasks/ci/.gitkeep create mode 100644 lib/tasks/ci/backup.rake create mode 100644 lib/tasks/ci/cleanup.rake create mode 100644 lib/tasks/ci/schedule_builds.rake create mode 100644 lib/tasks/ci/setup.rake create mode 100644 lib/tasks/ci/sidekiq.rake create mode 100644 public/ci/build-canceled.svg create mode 100644 public/ci/build-failed.svg create mode 100644 public/ci/build-pending.svg create mode 100644 public/ci/build-running.svg create mode 100644 public/ci/build-success.svg create mode 100644 public/ci/build-unknown.svg create mode 100644 public/ci/favicon.ico create mode 100755 scripts/ci/prepare_build.sh create mode 100644 spec/ci/controllers/commits_controller_spec.rb create mode 100644 spec/ci/controllers/projects_controller_spec.rb create mode 100644 spec/ci/factories/builds.rb create mode 100644 spec/ci/factories/commits.rb create mode 100644 spec/ci/factories/events.rb create mode 100644 spec/ci/factories/projects.rb create mode 100644 spec/ci/factories/runner_projects.rb create mode 100644 spec/ci/factories/runners.rb create mode 100644 spec/ci/factories/trigger_requests.rb create mode 100644 spec/ci/factories/triggers.rb create mode 100644 spec/ci/factories/users.rb create mode 100644 spec/ci/factories/web_hook.rb create mode 100644 spec/ci/features/admin/builds_spec.rb create mode 100644 spec/ci/features/admin/events_spec.rb create mode 100644 spec/ci/features/admin/projects_spec.rb create mode 100644 spec/ci/features/admin/runners_spec.rb create mode 100644 spec/ci/features/builds_spec.rb create mode 100644 spec/ci/features/commits_spec.rb create mode 100644 spec/ci/features/events_spec.rb create mode 100644 spec/ci/features/lint_spec.rb create mode 100644 spec/ci/features/projects_spec.rb create mode 100644 spec/ci/features/runners_spec.rb create mode 100644 spec/ci/features/triggers_spec.rb create mode 100644 spec/ci/features/variables_spec.rb create mode 100644 spec/ci/helpers/application_helper_spec.rb create mode 100644 spec/ci/helpers/runners_helper_spec.rb create mode 100644 spec/ci/helpers/user_helper_spec.rb create mode 100644 spec/ci/helpers/user_sessions_helper_spec.rb create mode 100644 spec/ci/lib/ansi2html_spec.rb create mode 100644 spec/ci/lib/charts_spec.rb create mode 100644 spec/ci/lib/gitlab_ci_yaml_processor_spec.rb create mode 100644 spec/ci/lib/upgrader_spec.rb create mode 100644 spec/ci/mailers/notify_spec.rb create mode 100644 spec/ci/models/build_spec.rb create mode 100644 spec/ci/models/commit_spec.rb create mode 100644 spec/ci/models/mail_service_spec.rb create mode 100644 spec/ci/models/network_spec.rb create mode 100644 spec/ci/models/project_services/hip_chat_message_spec.rb create mode 100644 spec/ci/models/project_services/hip_chat_service_spec.rb create mode 100644 spec/ci/models/project_services/slack_message_spec.rb create mode 100644 spec/ci/models/project_services/slack_service_spec.rb create mode 100644 spec/ci/models/project_spec.rb create mode 100644 spec/ci/models/runner_project_spec.rb create mode 100644 spec/ci/models/runner_spec.rb create mode 100644 spec/ci/models/service_spec.rb create mode 100644 spec/ci/models/trigger_spec.rb create mode 100644 spec/ci/models/user_spec.rb create mode 100644 spec/ci/models/variable_spec.rb create mode 100644 spec/ci/models/web_hook_spec.rb create mode 100644 spec/ci/requests/api/builds_spec.rb create mode 100644 spec/ci/requests/api/commits_spec.rb create mode 100644 spec/ci/requests/api/forks_spec.rb create mode 100644 spec/ci/requests/api/projects_spec.rb create mode 100644 spec/ci/requests/api/runners_spec.rb create mode 100644 spec/ci/requests/api/triggers_spec.rb create mode 100644 spec/ci/requests/builds_spec.rb create mode 100644 spec/ci/requests/commits_spec.rb create mode 100644 spec/ci/services/create_commit_service_spec.rb create mode 100644 spec/ci/services/create_project_service_spec.rb create mode 100644 spec/ci/services/create_trigger_request_service_spec.rb create mode 100644 spec/ci/services/event_service_spec.rb create mode 100644 spec/ci/services/image_for_build_service_spec.rb create mode 100644 spec/ci/services/register_build_service_spec.rb create mode 100644 spec/ci/services/web_hook_service_spec.rb create mode 100644 spec/ci/six.tar.gz create mode 100644 spec/ci/spec_helper.rb create mode 100644 spec/ci/support/api_helpers.rb create mode 100644 spec/ci/support/db_cleaner.rb create mode 100644 spec/ci/support/gitlab_stubs/gitlab_ci.yml create mode 100644 spec/ci/support/gitlab_stubs/project_8.json create mode 100644 spec/ci/support/gitlab_stubs/project_8_hooks.json create mode 100644 spec/ci/support/gitlab_stubs/projects.json create mode 100644 spec/ci/support/gitlab_stubs/raw_project.yml create mode 100644 spec/ci/support/gitlab_stubs/session.json create mode 100644 spec/ci/support/gitlab_stubs/user.json create mode 100644 spec/ci/support/login_helpers.rb create mode 100644 spec/ci/support/monkey_patches/oauth2.rb create mode 100644 spec/ci/support/setup_builds_storage.rb create mode 100644 spec/ci/support/stub_gitlab_calls.rb create mode 100644 spec/ci/support/stub_gitlab_data.rb diff --git a/CHANGELOG-CI b/CHANGELOG-CI new file mode 100644 index 00000000000..d1ad661d88b --- /dev/null +++ b/CHANGELOG-CI @@ -0,0 +1,298 @@ +v7.14.0 (unreleased) + - Truncate commit messages after subject line in table + - Adjust CI config to support Docker executors + - Added Application Settings + - Randomize test database for CI tests + - Make YAML validation stricter + - Use avatars received from GitLab + - Refactor GitLab API usage to use either access_token or private_token depending on what was specified during login + - Allow to use access_token for API requests + - Fix project API listing returning empty list when first projects are not added to CI + - Allow to define variables from YAML + - Added support for CI skipped status + - Fix broken yaml error saving + - Add committed_at to commits to properly order last commit (the force push issue) + - Rename type(s) to stage(s) + - Fix navigation icons + - Add missing stage when doing retry + - Require variable keys to be not-empty and unique + - Fix variable saving issue + - Display variable saving errors in variables page not the project's + - Added Build Triggers API + +v7.13.1 + - Fix: user could steal specific runner + - Fix: don't send notifications for jobs with allow_failure set + - Fix invalid link to doc.gitlab.com + +v7.13.0 + - Fix inline edit runner-description + - Allow to specify image and services in yml that can be used with docker + - Fix: No runner notification can see managers only + - Fix service testing for slack + - Ability to cancel all builds in commit at once + - Disable colors in rake tasks automatically (if IO is not a TTY) + - Implemented "rake env:info". Rake task to receive system information + - Fix coverage calculation on commit page + - Enhance YAML validation + - Redirect back after authorization + - Change favicon + - Refactoring: Get rid of private_token usage in the frontend. + - Allow to specify allow_failure for job + - Build traces is stored in the file instead of database + - Make the builds path configurable + - Disable link to runner if it's not assigned to specific project + - Store all secrets in config/secrets.yml + - Encrypt variables + - Allow to specify flexible list of types in yaml + +v7.12.2 + - Revert: Runner without tag should pick builds without tag only + +v7.12.1 + - Runner without tag should pick builds without tag only + - Explicit error in the GitLab when commit not found. + - Fix: lint with relative subpath + - Update webhook example + - Improved Lint stability + - Add warning when .gitlab-ci.yml not found + - Improved validation for .gitlab-ci.yml + - Fix list of branches in only section + - Fix "Status Badge" button + +v7.12.0 + - Endless scroll on the dashboard + - Add notification if there are no runners + - Fix pagination on dashboard + - Remove ID column from runners list in the admin area + - Increase default timeout for builds to 60 minutes + - Using .gitlab-ci.yml file instead of jobs + - Link to the runner from the build page for admin user + - Ability to set secret variables for runner + - Dont retry build when push same commit in same ref twice + - Admin area: show amount of runners with last contact less than a minute ago + - Fix re-adding project with the same name but different gitlab_id + - Implementation of Lint (.gitlab-ci.yml validation tool) + - Updated rails to 4.1.11 + - API fix: project create call + - Link to web-editor with .gitlab-ci.yml + - Updated examples in the documentation + +v7.11.0 + - Deploy Jobs API calls + - Projects search on dashboard page + - Improved runners page + - Running and Pending tabs on admin builds page + - Fix [ci skip] tag, so you can skip CI triggering now + - Add HipChat notifications + - Clean up project advanced settings. + - Add a GitLab project path parameter to the project API + - Remove projects IDs from dashboard + - UI fix: Remove page headers from the admin area + - Improve Email templates + - Add backup/restore utility + - Coordinator stores information(version, platform, revision, etc.) about runners. + - Fixed pagination on dashboard + - Public accessible build and commit pages of public projects + - Fix vulnerability in the API when MySQL is used + +v7.10.1 + - Fix failing migration when update to 7.10 from 7.8 and older versions + +sidekiq_wirker_fix + - added sidekiq.yml + - integrated in script/background_jobs +v7.10.0 + - Projects sorting by last commit date + - Add project search at runner page + - Fix GitLab and CI projects collision + - Events for admin + - Events per projects + - Search for runners in admin area + - UI improvements: created separated admin section, removed useless project show page + - Runners sorting in admin area (by id) + - Remove protected_attributes gem + - Skip commit creation if there is no appropriate job + +v7.9.3 + - Contains no changes + - Developers can cancel and retry jobs + +v7.9.2 + - [Security] Already existing projects should not be served by shared runners + - Ability to run deploy job without test jobs (every push will trigger deploy job) + +v7.9.1 + - [Security] Adding explicit is_shared parameter to runner + - [Security] By default new projects are not served by shared runners + +v7.9.0 + - Reset user session if token is invalid + - Runner delete api endpoint + - Fix bug about showing edit button on commit page if user does not have permissions + - Allow to pass description and tag list during Runner's registration + - Added api for project jobs + - Implementation of deploy jobs after all parallel jobs(tests). + - Add scroll up/down buttons for better mobile experience with large build traces + - Add runner last contact (Kamil Trzciński) + - Allow to pause runners - when paused runner will not receive any new build (Kamil Trzciński) + - Add brakeman (security scanner for Ruby on Rails) + - Changed a color of the canceled builds + - Fix of show the same commits in different branches + +v7.8.2 + - Fix the broken build failed email + - Notify only pusher instead of commiter + +v7.8.0 + - Fix OAuth login with GitLab installed in relative URL + - GitLab CI has same version as GitLab since now + - Allow to pass description and tag list during Runner's registration (Kamil Trzciński) + - Update documentation (API, Install, Update) + - Skip refs field supports for wildcard branch name (ex. feature/*) + - Migrate E-mail notification to Services menu (Kamil Trzciński) + - Added Slack notifications (Kamil Trzciński) + - Disable turbolink on links pointing out to GitLab server + - Add test coverage parsing example for pytest-cov + - Upgrade raindrops gem + +v5.4.2 + - Fix exposure of project token via build data + +v5.4.1 + - Fix 500 if on builds page if build has no job + - Truncate project token from build trace + - Allow users with access to project see build trace + +v5.4.0 (Requires GitLab 7.7) + - Fixed 500 error for badge if build is pending + - Non-admin users can now register specific runners for their projects + - Project specific runners page which users can access + - Remove progress output from schedule_builds cron job + - Fix schedule_builds rake task + - Fix test webhook button + - Job can be branch specific or tag specific or both + - Shared runners builds projects which are not assigned to specific ones + - Job can be runner specific through tags + - Runner have tags + - Move job settings to separate page + - Add authorization level managing projects + - OAuth authentication via GitLab. + +v5.3 + - Remove annoying 'Done' message from schedule_builds cron job + - Fix a style issue with the navbar + - Skip CSRF check on the project's build page + - Fix showing wrong build script on admin projects page + - Add branch and commit message to build result emails + +v5.2 + - Improve performance by adding new indicies + - Separate Commit logic from Build logic in prep for Parallel Builds + - Parallel builds + - You can have multiple build scripts per project + +v5.1 + - Registration token and runner token are named differently + - Redirect to previous page after sign-in + - Dont show archived projects + - Add support for skip branches from build + - Add coverage parsing feature + - Update rails to 4.0.10 + - Look for a REVISION file before running `git log` + - All builds page for admin + +v5.0.1 + - Update rails to 4.0.5 + +v5.0.0 + - Set build timeout in minutes + - Web Hooks for builds + - Nprogress bar + - Remove extra spaces in build script + - Requires runner v5 + * All script commands executed as one file + * Cancel button works correctly now + * Runner stability increased + * Timeout applies to build now instead of line of script + +v4.3.0 + - Refactor build js + - Redirect to build page with sha + bid if build id is not provided + - Update rails to 4.0.3 + - Restyle project settings page + - Improve help page + - Replaced puma with unicorn + - Improved init.d script + - Add submodule init to default build script for new projects + +v4.2.0 + - Build duration chart + - Bootstrap 3 with responsive UI + - Improved init.d script + - Refactoring + - Changed http codes for POST /projects/:id/build action + - Turbolinks + +v4.1.0 + - Rails 4 + - Click on build branch to see other builds for this branch + - Email notifications (Jeroen Knoops) + +v4.0.0 + - Shared runners (no need to add runner to every project) + - Admin area (only available for GitLab admins) + - Hide all runners management into admin area + - Use http cloning for builds instead of deploy keys + - Allow choose between git clone and git fetch when get code for build + - Make build timeout actually works + - Requires GitLab 6.3 or higher + - GitLab CI settings go to GitLab project via api on creation + +v3.2.0 + - Limit visibility of projects by gitlab authorized projects + - Use one page for both gitlab and gitlab-ci projects + +v3.1.0 + - Login with both username, email or LDAP credentials (if GitLab 6.0+) + - Retry build button functionality + - UI fixes for resolution 1366px and lower + - Fix gravatar ssl warning + +v3.0.0 + - Build running functionality extracted in gitlab-ci-runner + - Added API for runners and builds + - Redesigned application + - Added charts + - Use GitLab auth + - Add projects via UI with few clicks + +v2.2.0 + - replaced unicorn with puma + - replaced grit with rugged + - Runner.rb more transactional safe now + - updated rails to 3.2.13 + - updated devise to 2.2 + - fixed issue when build left in running status if exception triggered + - rescue build timeout correctly + - badge helper with markdown & html + - increased test coverage to 85% + +v2.1.0 + - Removed horizontal scroll for build trace + - new status badges + - better encode + - added several CI_* env variables + +v2.0.0 + - Replace resque with sidekiq + - Run only one build at time per project + - Added whenever for schedule jobs + +v1.2.0 + - Added Github web hook support + - Added build schedule + +v1.1.0 + - Added JSON response for builds status + - Compatible with GitLab v4.0.0 \ No newline at end of file diff --git a/Gemfile b/Gemfile index 3aa3c72e088..6933fc92ddc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,14 @@ source "https://rubygems.org" -gem 'rails', '4.1.11' +def darwin_only(require_as) + RUBY_PLATFORM.include?('darwin') && require_as +end + +def linux_only(require_as) + RUBY_PLATFORM.include?('linux') && require_as +end + +gem 'rails', '4.1.12' # Specify a sprockets version due to security issue # See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY @@ -10,28 +18,28 @@ gem 'sprockets', '~> 2.12.3' gem "default_value_for", "~> 3.0.0" # Supported DBs -gem "mysql2", group: :mysql -gem "pg", group: :postgres +gem "mysql2", '~> 0.3.16', group: :mysql +gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem "devise", '3.2.4' -gem "devise-async", '0.9.0' +gem "devise", '~> 3.2.4' +gem "devise-async", '~> 0.9.0' gem 'omniauth', "~> 1.2.2" -gem 'omniauth-google-oauth2' -gem 'omniauth-twitter' -gem 'omniauth-github' -gem 'omniauth-shibboleth' -gem 'omniauth-kerberos', group: :kerberos -gem 'omniauth-gitlab' -gem 'omniauth-bitbucket' +gem 'omniauth-google-oauth2', '~> 0.2.5' +gem 'omniauth-twitter', '~> 1.0.1' +gem 'omniauth-github', '~> 1.1.1' +gem 'omniauth-shibboleth', '~> 1.1.1' +gem 'omniauth-kerberos', '~> 0.2.0', group: :kerberos +gem 'omniauth-gitlab', '~> 1.0.0' +gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-saml', '~> 1.4.0' -gem 'doorkeeper', '2.1.3' +gem 'doorkeeper', '~> 2.1.3' gem "rack-oauth2", "~> 1.0.5" # Two-factor authentication -gem 'devise-two-factor' -gem 'rqrcode-rails3' -gem 'attr_encrypted', '1.3.4' +gem 'devise-two-factor', '~> 1.0.1' +gem 'rqrcode-rails3', '~> 0.1.7' +gem 'attr_encrypted', '~> 1.3.4' # Browser detection gem "browser", '~> 0.8.0' @@ -48,7 +56,7 @@ gem 'gitlab-grack', '~> 2.0.2', require: 'grack' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes # see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master -gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" +gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap" # Git Wiki gem 'gollum-lib', '~> 4.0.2' @@ -63,47 +71,47 @@ gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API gem "grape", "~> 0.6.1" gem "grape-entity", "~> 0.4.2" -gem 'rack-cors', require: 'rack/cors' +gem 'rack-cors', '~> 0.2.9', require: 'rack/cors' # Format dates and times # based on human-friendly examples -gem "stamp" +gem "stamp", '~> 0.5.0' # Enumeration fields -gem 'enumerize' +gem 'enumerize', '~> 0.7.0' # Pagination gem "kaminari", "~> 0.15.1" # HAML -gem "haml-rails" +gem "haml-rails", '~> 0.5.3' # Files attachments -gem "carrierwave" +gem "carrierwave", '~> 0.9.0' # Drag and Drop UI -gem 'dropzonejs-rails' +gem 'dropzonejs-rails', '~> 0.7.1' # for aws storage gem "fog", "~> 1.25.0" -gem "unf" +gem "unf", '~> 0.1.4' # Authorization -gem "six" +gem "six", '~> 0.2.0' # Seed data -gem "seed-fu" +gem "seed-fu", '~> 2.3.5' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' -gem 'task_list', '1.0.2', require: 'task_list/railtie' -gem 'github-markup' +gem 'task_list', '~> 1.0.2', require: 'task_list/railtie' +gem 'github-markup', '~> 1.3.1' gem 'redcarpet', '~> 3.3.2' -gem 'RedCloth' +gem 'RedCloth', '~> 4.2.9' gem 'rdoc', '~>3.6' -gem 'org-ruby', '= 0.9.12' +gem 'org-ruby', '~> 0.9.12' gem 'creole', '~>0.3.6' -gem 'wikicloth', '=0.8.1' +gem 'wikicloth', '~> 0.8.1' gem 'asciidoctor', '~> 1.5.2' # Diffs @@ -111,37 +119,37 @@ gem 'diffy', '~> 3.0.3' # Application server group :unicorn do - gem "unicorn", '~> 4.6.3' - gem 'unicorn-worker-killer' + gem "unicorn", '~> 4.8.2' + gem 'unicorn-worker-killer', '~> 0.4.2' end # State machine -gem "state_machine" +gem "state_machine", '~> 1.2.0' # Issue tags gem 'acts-as-taggable-on', '~> 3.4' # Background jobs -gem 'slim' -gem 'sinatra', require: nil +gem 'slim', '~> 2.0.2' +gem 'sinatra', '~> 1.4.4', require: nil gem 'sidekiq', '~> 3.3' -gem 'sidetiq', '0.6.3' +gem 'sidetiq', '~> 0.6.3' # HTTP requests -gem "httparty" +gem "httparty", '~> 0.13.3' # Colored output to console -gem "colored" +gem "colored", '~> 1.2' # GitLab settings -gem 'settingslogic' +gem 'settingslogic', '~> 2.0.9' # Misc -gem "foreman" -gem 'version_sorter' + +gem 'version_sorter', '~> 2.0.0' # Cache -gem "redis-rails" +gem "redis-rails", '~> 4.0.0' # Campfire integration gem 'tinder', '~> 1.9.2' @@ -177,69 +185,70 @@ gem "sanitize", '~> 2.0' gem "rack-attack", '~> 4.3.0' # Ace editor -gem 'ace-rails-ap' +gem 'ace-rails-ap', '~> 2.0.1' # Keyboard shortcuts -gem 'mousetrap-rails' +gem 'mousetrap-rails', '~> 1.4.6' # Detect and convert string character encoding -gem 'charlock_holmes' +gem 'charlock_holmes', '~> 0.6.9.4' gem "sass-rails", '~> 4.0.5' -gem "coffee-rails" -gem "uglifier" +gem "coffee-rails", '~> 4.1.0' +gem "uglifier", '~> 2.3.2' gem 'turbolinks', '~> 2.5.0' -gem 'jquery-turbolinks' +gem 'jquery-turbolinks', '~> 2.0.1' -gem 'addressable' +gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.0' gem 'font-awesome-rails', '~> 4.2' gem 'gitlab_emoji', '~> 0.1' gem 'gon', '~> 5.0.0' gem 'jquery-atwho-rails', '~> 1.0.0' -gem 'jquery-rails', '3.1.3' -gem 'jquery-scrollto-rails' -gem 'jquery-ui-rails' -gem 'nprogress-rails' +gem 'jquery-rails', '~> 3.1.3' +gem 'jquery-scrollto-rails', '~> 1.4.3' +gem 'jquery-ui-rails', '~> 4.2.1' +gem 'nprogress-rails', '~> 0.1.2.3' gem 'raphael-rails', '~> 2.1.2' -gem 'request_store' +gem 'request_store', '~> 1.2.0' gem 'select2-rails', '~> 3.5.9' -gem 'virtus' +gem 'virtus', '~> 1.0.1' group :development do + gem "foreman" gem 'brakeman', require: false - gem "annotate", "~> 2.6.0.beta2" - gem "letter_opener" - gem 'quiet_assets', '~> 1.0.1' - gem 'rack-mini-profiler', require: false + + gem "annotate", "~> 2.6.0" + gem "letter_opener", '~> 1.1.2' + gem 'quiet_assets', '~> 1.0.2' + gem 'rack-mini-profiler', '~> 0.9.0', require: false gem 'rerun', '~> 0.10.0' # Better errors handler - gem 'better_errors' - gem 'binding_of_caller' + gem 'better_errors', '~> 1.0.1' + gem 'binding_of_caller', '~> 0.7.2' # Docs generator - gem "sdoc" + gem "sdoc", '~> 0.3.20' # thin instead webrick - gem 'thin' + gem 'thin', '~> 1.6.1' end group :development, :test do - gem 'awesome_print' gem 'byebug', platform: :mri - gem 'fuubar', '~> 2.0.0' gem 'pry-rails' - gem 'coveralls', '~> 0.8.2', require: false + gem 'awesome_print', '~> 1.2.0' + gem 'fuubar', '~> 2.0.0' + gem 'database_cleaner', '~> 1.4.0' - gem 'factory_girl_rails' + gem 'factory_girl_rails', '~> 4.3.0' gem 'rspec-rails', '~> 3.3.0' - gem 'rubocop', '0.28.0', require: false - gem 'spinach-rails' + gem 'spinach-rails', '~> 0.2.1' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) - gem 'minitest', '~> 5.3.0' + gem 'minitest', '~> 5.7.0' # Generate Fake data gem 'ffaker', '~> 2.0.0' @@ -249,30 +258,57 @@ group :development, :test do gem 'poltergeist', '~> 1.6.0' gem 'teaspoon', '~> 1.0.0' - gem 'teaspoon-jasmine' + gem 'teaspoon-jasmine', '~> 2.2.0' - gem 'spring', '~> 1.3.1' - gem 'spring-commands-rspec', '~> 1.0.0' + gem 'spring', '~> 1.3.6' + gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-spinach', '~> 1.0.0' gem 'spring-commands-teaspoon', '~> 0.0.2' + + gem 'rubocop', '~> 0.28.0', require: false + gem 'coveralls', '~> 0.8.2', require: false + gem 'simplecov', '~> 0.10.0', require: false end group :test do - gem 'simplecov', require: false gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'email_spec', '~> 1.6.0' gem 'webmock', '~> 1.21.0' - gem 'test_after_commit' + gem 'test_after_commit', '~> 0.2.2' end group :production do gem "gitlab_meta", '7.0' end -gem "newrelic_rpm" +gem "newrelic_rpm", '~> 3.9.4.245' -gem 'octokit', '3.7.0' +gem 'octokit', '~> 3.7.0' gem "mail_room", "~> 0.4.0" -gem 'email_reply_parser' +gem 'email_reply_parser', '~> 0.5.8' + +## CI +gem 'activerecord-deprecated_finders', '~> 1.0.3' +gem 'activerecord-session_store', '~> 0.1.0' +gem "nested_form", '~> 0.3.2' + +# Scheduled +gem 'whenever', '~> 0.8.4', require: false + +# OAuth +gem 'oauth2', '~> 1.0.0' + +gem 'gitlab_ci_meta', '~> 4.0' + +# Soft deletion +gem "paranoia", "~> 2.0" + +group :development, :test do + gem 'guard-rspec', '~> 4.2.0' + + gem 'rb-fsevent', require: darwin_only('rb-fsevent') + gem 'growl', require: darwin_only('growl') + gem 'rb-inotify', require: linux_only('rb-inotify') +end diff --git a/Gemfile.lock b/Gemfile.lock index 5278fe243a8..b810f2fdd4e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,31 +4,36 @@ GEM CFPropertyList (2.3.1) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.1.11) - actionpack (= 4.1.11) - actionview (= 4.1.11) + actionmailer (4.1.12) + actionpack (= 4.1.12) + actionview (= 4.1.12) mail (~> 2.5, >= 2.5.4) - actionpack (4.1.11) - actionview (= 4.1.11) - activesupport (= 4.1.11) + actionpack (4.1.12) + actionview (= 4.1.12) + activesupport (= 4.1.12) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.11) - activesupport (= 4.1.11) + actionview (4.1.12) + activesupport (= 4.1.12) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.11) - activesupport (= 4.1.11) + activemodel (4.1.12) + activesupport (= 4.1.12) builder (~> 3.1) - activerecord (4.1.11) - activemodel (= 4.1.11) - activesupport (= 4.1.11) + activerecord (4.1.12) + activemodel (= 4.1.12) + activesupport (= 4.1.12) arel (~> 5.0.0) + activerecord-deprecated_finders (1.0.4) + activerecord-session_store (0.1.1) + actionpack (>= 4.0.0, < 5) + activerecord (>= 4.0.0, < 5) + railties (>= 4.0.0, < 5) activeresource (4.0.0) activemodel (~> 4.0) activesupport (~> 4.0) rails-observers (~> 0.1.1) - activesupport (4.1.11) + activesupport (4.1.12) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -37,50 +42,49 @@ GEM acts-as-taggable-on (3.5.0) activerecord (>= 3.2, < 5) addressable (2.3.8) - annotate (2.6.0) - activerecord (>= 2.3.0) - rake (>= 0.8.7) + annotate (2.6.10) + activerecord (>= 3.2, <= 4.3) + rake (~> 10.4) arel (5.0.1.20140414130214) asana (0.0.6) activeresource (>= 3.2.3) asciidoctor (1.5.2) - ast (2.0.0) - astrolabe (1.3.0) - parser (>= 2.2.0.pre.3, < 3.0) + ast (2.1.0) + astrolabe (1.3.1) + parser (~> 2.2) attr_encrypted (1.3.4) encryptor (>= 1.3.0) attr_required (1.0.0) - autoprefixer-rails (5.1.11) + autoprefixer-rails (5.2.1.2) execjs json awesome_print (1.2.0) - axiom-types (0.0.5) - descendants_tracker (~> 0.0.1) - ice_nine (~> 0.9) - bcrypt (3.1.7) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + bcrypt (3.1.10) better_errors (1.0.1) coderay (>= 1.0.0) erubis (>= 2.6.6) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootstrap-sass (3.3.4.1) + bootstrap-sass (3.3.5) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.2.19) - brakeman (3.0.1) + brakeman (3.0.5) erubis (~> 2.6) fastercsv (~> 1.5) haml (>= 3.0, < 5.0) highline (~> 1.6.20) multi_json (~> 1.2) ruby2ruby (~> 2.1.1) - ruby_parser (~> 3.5.0) + ruby_parser (~> 3.7.0) sass (~> 3.0) terminal-table (~> 1.4) browser (0.8.0) builder (3.2.2) - byebug (3.2.0) - columnize (~> 0.8) - debugger-linecache (~> 1.2) + byebug (6.0.2) cal-heatmap-rails (0.0.1) capybara (2.4.4) mime-types (>= 1.16) @@ -88,7 +92,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara-screenshot (1.0.9) + capybara-screenshot (1.0.11) capybara (>= 1.0, < 3) launchy carrierwave (0.9.0) @@ -98,6 +102,8 @@ GEM celluloid (0.16.0) timers (~> 4.0.0) charlock_holmes (0.6.9.4) + chronic (0.10.2) + chunky_png (1.3.4) cliver (0.3.2) coderay (1.1.0) coercible (1.0.0) @@ -110,9 +116,8 @@ GEM execjs coffee-script-source (1.9.1.1) colored (1.2) - colorize (0.5.8) - columnize (0.9.0) - connection_pool (2.1.0) + colorize (0.7.7) + connection_pool (2.2.0) coveralls (0.8.2) json (~> 1.8) rest-client (>= 1.6.8, < 2) @@ -122,15 +127,15 @@ GEM crack (0.4.2) safe_yaml (~> 1.0.0) creole (0.3.8) - d3_rails (3.5.5) + d3_rails (3.5.6) railties (>= 3.1.0) - daemons (1.1.9) + daemons (1.2.3) database_cleaner (1.4.1) debug_inspector (0.0.2) - debugger-linecache (1.2.0) - default_value_for (3.0.0) + default_value_for (3.0.1) activerecord (>= 3.2.0, < 5.0) - descendants_tracker (0.0.3) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) devise (3.2.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -139,21 +144,20 @@ GEM warden (~> 1.2.3) devise-async (0.9.0) devise (~> 3.2) - devise-two-factor (1.0.1) + devise-two-factor (1.0.2) activemodel activesupport attr_encrypted (~> 1.3.2) - devise (~> 3.2.4) - rails - rotp (~> 1.6.1) + devise (>= 3.2.4, < 3.5) + railties + rotp (< 2) diff-lcs (1.2.5) - diffy (3.0.3) + diffy (3.0.7) docile (1.1.5) domain_name (0.5.24) unf (>= 0.0.5, < 1.0.0) - doorkeeper (2.1.3) + doorkeeper (2.1.4) railties (>= 3.2) - dotenv (0.9.0) dropzonejs-rails (0.7.1) rails (> 3.1) email_reply_parser (0.5.8) @@ -163,25 +167,25 @@ GEM encryptor (1.3.0) enumerize (0.7.0) activesupport (>= 3.2) - equalizer (0.0.8) + equalizer (0.0.11) erubis (2.7.0) escape_utils (0.2.4) - eventmachine (1.0.4) - excon (0.45.3) - execjs (2.5.2) + eventmachine (1.0.8) + excon (0.45.4) + execjs (2.6.0) expression_parser (0.9.0) factory_girl (4.3.0) activesupport (>= 3.0.0) factory_girl_rails (4.3.0) factory_girl (~> 4.3.0) railties (>= 3.0.0) - faraday (0.8.9) + faraday (0.8.10) multipart-post (~> 1.2.0) - faraday_middleware (0.9.0) - faraday (>= 0.7.4, < 0.9) + faraday_middleware (0.10.0) + faraday (>= 0.7.4, < 0.10) fastercsv (1.5.5) ffaker (2.0.0) - ffi (1.9.8) + ffi (1.9.10) fission (0.5.0) CFPropertyList (~> 2.2) flowdock (0.7.0) @@ -202,11 +206,11 @@ GEM ipaddress (~> 0.5) nokogiri (~> 1.5, >= 1.5.11) opennebula - fog-brightbox (0.7.1) + fog-brightbox (0.9.0) fog-core (~> 1.22) fog-json inflecto (~> 0.0.2) - fog-core (1.30.0) + fog-core (1.32.1) builder excon (~> 0.45) formatador (~> 0.2) @@ -216,7 +220,7 @@ GEM fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-profitbricks (0.0.3) + fog-profitbricks (0.0.5) fog-core fog-xml nokogiri @@ -227,7 +231,7 @@ GEM fog-sakuracloud (1.0.1) fog-core fog-json - fog-softlayer (0.4.6) + fog-softlayer (0.4.7) fog-core fog-json fog-terremark (0.1.0) @@ -242,30 +246,28 @@ GEM fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) - font-awesome-rails (4.2.0.0) + font-awesome-rails (4.4.0.0) railties (>= 3.2, < 5.0) - foreman (0.63.0) - dotenv (>= 0.7) - thor (>= 0.13.6) + foreman (0.78.0) + thor (~> 0.19.1) formatador (0.2.5) fuubar (2.0.0) rspec (~> 3.0) ruby-progressbar (~> 1.4) gemnasium-gitlab-service (0.2.6) rugged (~> 0.21) - gemojione (2.0.0) + gemojione (2.0.1) json - gherkin-ruby (0.3.1) - racc - github-markup (1.3.1) - posix-spawn (~> 0.3.8) + get_process_mem (0.2.0) + gherkin-ruby (0.3.2) + github-markup (1.3.3) gitlab-flowdock-git-hook (1.0.1) flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json gitlab-grack (2.0.2) rack (~> 1.5.1) - gitlab-grit (2.7.2) + gitlab-grit (2.7.3) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) mime-types (~> 1.15) @@ -274,6 +276,7 @@ GEM charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.4) mime-types (~> 1.19) + gitlab_ci_meta (4.0) gitlab_emoji (0.1.0) gemojione (~> 2.0) gitlab_git (7.2.14) @@ -287,16 +290,16 @@ GEM omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) rubyntlm (~> 0.3) - gollum-grit_adapter (0.1.3) + gollum-grit_adapter (1.0.0) gitlab-grit (~> 2.7, >= 2.7.1) - gollum-lib (4.0.2) - github-markup (~> 1.3.1) - gollum-grit_adapter (~> 0.1, >= 0.1.1) + gollum-lib (4.0.3) + github-markup (~> 1.3.3) + gollum-grit_adapter (~> 1.0) nokogiri (~> 1.6.4) - rouge (~> 1.9) + rouge (~> 1.7.4) sanitize (~> 2.1.0) stringex (~> 2.5.1) - gon (5.0.1) + gon (5.0.4) actionpack (>= 2.3.0) json grape (0.6.1) @@ -309,9 +312,22 @@ GEM rack-accept rack-mount virtus (>= 1.0.0) - grape-entity (0.4.2) + grape-entity (0.4.8) activesupport multi_json (>= 1.3.2) + growl (1.0.3) + guard (2.13.0) + formatador (>= 0.2.4) + listen (>= 2.7, <= 4.0) + lumberjack (~> 1.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-rspec (4.2.10) + guard (~> 2.1) + rspec (>= 2.14, < 4.0) haml (4.0.7) tilt haml-rails (0.5.3) @@ -322,24 +338,24 @@ GEM hashie (2.1.2) highline (1.6.21) hike (1.2.3) - hipchat (1.5.0) + hipchat (1.5.2) httparty mimemagic hitimes (1.2.2) html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) + htmlentities (4.3.4) http-cookie (1.0.2) domain_name (~> 0.5) http_parser.rb (0.5.3) - httparty (0.13.3) + httparty (0.13.5) json (~> 1.8) multi_xml (>= 0.5.2) - httpauth (0.2.1) - httpclient (2.5.3.3) + httpclient (2.6.0.1) i18n (0.7.0) ice_cube (0.11.1) - ice_nine (0.10.0) + ice_nine (0.11.1) inflecto (0.0.2) ipaddress (0.8.0) jquery-atwho-rails (1.0.1) @@ -348,26 +364,26 @@ GEM thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) railties (> 3.1, < 5.0) - jquery-turbolinks (2.0.1) + jquery-turbolinks (2.0.2) railties (>= 3.1.0) turbolinks jquery-ui-rails (4.2.1) railties (>= 3.2.16) json (1.8.3) - jwt (0.1.13) - multi_json (>= 1.5) + jwt (1.5.1) kaminari (0.15.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.9.2) + kgio (2.9.3) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - listen (2.10.0) + listen (2.10.1) celluloid (~> 0.16.0) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) + lumberjack (1.0.9) macaddr (1.7.1) systemu (~> 2.6.2) mail (2.6.3) @@ -377,12 +393,14 @@ GEM mime-types (1.25.1) mimemagic (0.3.0) mini_portile (0.6.2) - minitest (5.3.5) + minitest (5.7.0) mousetrap-rails (1.4.6) multi_json (1.11.2) multi_xml (0.5.5) multipart-post (1.2.0) - mysql2 (0.3.16) + mysql2 (0.3.20) + nenv (0.2.0) + nested_form (0.3.2) net-ldap (0.11) net-scp (1.2.1) net-ssh (>= 2.6.5) @@ -391,15 +409,18 @@ GEM newrelic_rpm (3.9.4.245) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + notiffany (0.0.7) + nenv (~> 0.1) + shellany (~> 0.0) nprogress-rails (0.1.2.3) oauth (0.4.7) - oauth2 (0.8.1) - faraday (~> 0.8) - httpauth (~> 0.1) - jwt (~> 0.1.4) - multi_json (~> 1.0) + oauth2 (1.0.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) rack (~> 1.2) - octokit (3.7.0) + octokit (3.7.1) sawyer (~> 0.6.0, >= 0.5.3) omniauth (1.2.2) hashie (>= 1.2, < 4) @@ -408,30 +429,30 @@ GEM multi_json (~> 1.7) omniauth (~> 1.1) omniauth-oauth (~> 1.0) - omniauth-github (1.1.1) + omniauth-github (1.1.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) omniauth-gitlab (1.0.0) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) - omniauth-google-oauth2 (0.2.5) + omniauth-google-oauth2 (0.2.6) omniauth (> 1.0) omniauth-oauth2 (~> 1.1) omniauth-kerberos (0.2.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) - omniauth-multipassword (0.4.1) + omniauth-multipassword (0.4.2) omniauth (~> 1.0) - omniauth-oauth (1.0.1) + omniauth-oauth (1.1.0) oauth omniauth (~> 1.0) - omniauth-oauth2 (1.1.1) - oauth2 (~> 0.8.0) - omniauth (~> 1.0) + omniauth-oauth2 (1.3.1) + oauth2 (~> 1.0) + omniauth (~> 1.2) omniauth-saml (1.4.1) omniauth (~> 1.1) ruby-saml (~> 1.0.0) - omniauth-shibboleth (1.1.1) + omniauth-shibboleth (1.1.2) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) multi_json (~> 1.3) @@ -443,7 +464,9 @@ GEM org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) - parser (2.2.0.2) + paranoia (2.1.3) + activerecord (~> 4.0) + parser (2.2.2.6) ast (>= 1.1, < 3.0) pg (0.18.2) poltergeist (1.6.0) @@ -451,60 +474,59 @@ GEM cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - posix-spawn (0.3.9) + posix-spawn (0.3.11) powerpack (0.0.9) - pry (0.9.12.4) - coderay (~> 1.0) - method_source (~> 0.8) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) slop (~> 3.4) - pry-rails (0.3.2) + pry-rails (0.3.4) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) - quiet_assets (1.0.2) + quiet_assets (1.0.3) railties (>= 3.1, < 5.0) - racc (1.4.10) rack (1.5.5) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.3.0) rack rack-cors (0.2.9) - rack-mini-profiler (0.9.0) + rack-mini-profiler (0.9.7) rack (>= 1.1.3) rack-mount (0.8.3) rack (>= 1.0.0) - rack-oauth2 (1.0.8) + rack-oauth2 (1.0.10) activesupport (>= 2.3) attr_required (>= 0.0.5) - httpclient (>= 2.2.0.2) + httpclient (>= 2.4) multi_json (>= 1.3.6) rack (>= 1.1) - rack-protection (1.5.1) + rack-protection (1.5.3) rack rack-test (0.6.3) rack (>= 1.0) - rails (4.1.11) - actionmailer (= 4.1.11) - actionpack (= 4.1.11) - actionview (= 4.1.11) - activemodel (= 4.1.11) - activerecord (= 4.1.11) - activesupport (= 4.1.11) + rails (4.1.12) + actionmailer (= 4.1.12) + actionpack (= 4.1.12) + actionview (= 4.1.12) + activemodel (= 4.1.12) + activerecord (= 4.1.12) + activesupport (= 4.1.12) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.11) + railties (= 4.1.12) sprockets-rails (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) - railties (4.1.11) - actionpack (= 4.1.11) - activesupport (= 4.1.11) + railties (4.1.12) + actionpack (= 4.1.12) + activesupport (= 4.1.12) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) - raindrops (0.13.0) + raindrops (0.15.0) rake (10.4.2) raphael-rails (2.1.2) - rb-fsevent (0.9.4) + rb-fsevent (0.9.5) rb-inotify (0.9.5) ffi (>= 0.5.0) rbvmomi (1.8.2) @@ -519,10 +541,10 @@ GEM actionpack (~> 4) redis-rack (~> 1.5.0) redis-store (~> 1.1.0) - redis-activesupport (4.0.0) + redis-activesupport (4.1.1) activesupport (~> 4) redis-store (~> 1.1.0) - redis-namespace (1.5.1) + redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) redis-rack (1.5.0) rack (~> 1.5) @@ -533,32 +555,32 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.6) redis (>= 2.2) - request_store (1.0.5) + request_store (1.2.0) rerun (0.10.0) listen (~> 2.7, >= 2.7.3) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) - rinku (1.7.3) rotp (1.6.1) - rouge (1.9.1) - rqrcode (0.4.2) + rouge (1.7.7) + rqrcode (0.7.0) + chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) rspec (3.3.0) rspec-core (~> 3.3.0) rspec-expectations (~> 3.3.0) rspec-mocks (~> 3.3.0) - rspec-core (3.3.1) + rspec-core (3.3.2) rspec-support (~> 3.3.0) - rspec-expectations (3.3.0) + rspec-expectations (3.3.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.3.0) - rspec-mocks (3.3.0) + rspec-mocks (3.3.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.3.0) - rspec-rails (3.3.2) + rspec-rails (3.3.3) actionpack (>= 3.0, < 4.3) activesupport (>= 3.0, < 4.3) railties (>= 3.0, < 4.3) @@ -573,16 +595,16 @@ GEM powerpack (~> 0.0.6) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) - ruby-progressbar (1.7.1) + ruby-progressbar (1.7.5) ruby-saml (1.0.0) nokogiri (>= 1.5.10) uuid (~> 2.3) - ruby2ruby (2.1.3) + ruby2ruby (2.1.4) ruby_parser (~> 3.1) sexp_processor (~> 4.0) - ruby_parser (3.5.0) + ruby_parser (3.7.1) sexp_processor (~> 4.1) - rubyntlm (0.5.0) + rubyntlm (0.5.2) rubypants (0.2.0) rugged (0.22.2) safe_yaml (1.0.4) @@ -606,15 +628,16 @@ GEM select2-rails (3.5.9.3) thor (~> 0.14) settingslogic (2.0.9) - sexp_processor (4.4.5) + sexp_processor (4.6.0) + shellany (0.0.1) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (3.3.0) - celluloid (>= 0.16.0) - connection_pool (>= 2.0.0) - json - redis (>= 3.0.6) - redis-namespace (>= 1.3.1) + sidekiq (3.4.2) + celluloid (~> 0.16.0) + connection_pool (~> 2.2, >= 2.2.0) + json (~> 1.0) + redis (~> 3.2, >= 3.2.1) + redis-namespace (~> 1.5, >= 1.5.2) sidetiq (0.6.3) celluloid (>= 0.14.1) ice_cube (= 0.11.1) @@ -625,19 +648,20 @@ GEM json (~> 1.8) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) - sinatra (1.4.4) + sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) + tilt (>= 1.3, < 3) six (0.2.0) slack-notifier (1.0.0) - slim (2.0.2) + slim (2.0.3) temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) slop (3.6.0) - spinach (0.8.7) - colorize (= 0.5.8) - gherkin-ruby (>= 0.3.1) + spinach (0.8.10) + colorize + gherkin-ruby (>= 0.3.2) + json spinach-rails (0.2.1) capybara (>= 2.0.0) railties (>= 3) @@ -668,31 +692,32 @@ GEM railties (>= 3.2.5, < 5) teaspoon-jasmine (2.2.0) teaspoon (>= 1.0.0) - temple (0.6.7) + temple (0.6.10) term-ansicolor (1.3.2) tins (~> 1.0) - terminal-table (1.4.5) - test_after_commit (0.2.2) - thin (1.6.1) - daemons (>= 1.0.9) - eventmachine (>= 1.0.0) - rack (>= 1.0.0) + terminal-table (1.5.2) + test_after_commit (0.2.7) + activerecord (>= 3.2) + thin (1.6.3) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0) + rack (~> 1.0) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) timers (4.0.1) hitimes timfel-krb5-auth (0.8.3) - tinder (1.9.3) + tinder (1.9.4) eventmachine (~> 1.0) - faraday (~> 0.8) + faraday (~> 0.8.9) faraday_middleware (~> 0.9) hashie (>= 1.0, < 3) json (~> 1.8.0) mime-types (~> 1.19) multi_json (~> 1.7) twitter-stream (~> 0.1) - tins (1.5.4) + tins (1.6.0) trollop (2.1.2) turbolinks (2.5.3) coffee-rails @@ -700,41 +725,49 @@ GEM eventmachine (>= 0.12.8) http_parser.rb (~> 0.5.1) simple_oauth (~> 0.1.4) + twitter-text (1.12.0) + unf (~> 0.1.0) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.3.2) + uglifier (2.3.3) execjs (>= 0.3.0) json (>= 1.8.0) underscore-rails (1.4.4) unf (0.1.4) unf_ext unf_ext (0.0.7.1) - unicorn (4.6.3) + unicorn (4.8.3) kgio (~> 2.6) rack raindrops (~> 0.7) - unicorn-worker-killer (0.4.2) + unicorn-worker-killer (0.4.3) + get_process_mem (~> 0) unicorn (~> 4) uuid (2.3.8) macaddr (~> 1.0) version_sorter (2.0.0) - virtus (1.0.1) - axiom-types (~> 0.0.5) + virtus (1.0.5) + axiom-types (~> 0.1) coercible (~> 1.0) - descendants_tracker (~> 0.0.1) - equalizer (~> 0.0.7) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) warden (1.2.3) rack (>= 1.0) webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) - websocket-driver (0.5.4) + websocket-driver (0.6.2) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - wikicloth (0.8.1) + whenever (0.8.4) + activesupport (>= 2.3.4) + chronic (>= 0.6.3) + wikicloth (0.8.3) builder expression_parser - rinku + htmlentities + nokogiri + twitter-text xpath (2.0.0) nokogiri (~> 1.3) @@ -742,17 +775,19 @@ PLATFORMS ruby DEPENDENCIES - RedCloth - ace-rails-ap + RedCloth (~> 4.2.9) + ace-rails-ap (~> 2.0.1) + activerecord-deprecated_finders (~> 1.0.3) + activerecord-session_store (~> 0.1.0) acts-as-taggable-on (~> 3.4) - addressable - annotate (~> 2.6.0.beta2) + addressable (~> 2.3.8) + annotate (~> 2.6.0) asana (~> 0.0.6) asciidoctor (~> 1.5.2) - attr_encrypted (= 1.3.4) - awesome_print - better_errors - binding_of_caller + attr_encrypted (~> 1.3.4) + awesome_print (~> 1.2.0) + better_errors (~> 1.0.1) + binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.0) brakeman browser (~> 0.8.0) @@ -760,127 +795,136 @@ DEPENDENCIES cal-heatmap-rails (~> 0.0.1) capybara (~> 2.4.0) capybara-screenshot (~> 1.0.0) - carrierwave - charlock_holmes - coffee-rails - colored + carrierwave (~> 0.9.0) + charlock_holmes (~> 0.6.9.4) + coffee-rails (~> 4.1.0) + colored (~> 1.2) coveralls (~> 0.8.2) creole (~> 0.3.6) d3_rails (~> 3.5.5) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) - devise (= 3.2.4) - devise-async (= 0.9.0) - devise-two-factor + devise (~> 3.2.4) + devise-async (~> 0.9.0) + devise-two-factor (~> 1.0.1) diffy (~> 3.0.3) - doorkeeper (= 2.1.3) - dropzonejs-rails - email_reply_parser + doorkeeper (~> 2.1.3) + dropzonejs-rails (~> 0.7.1) + email_reply_parser (~> 0.5.8) email_spec (~> 1.6.0) - enumerize - factory_girl_rails + enumerize (~> 0.7.0) + factory_girl_rails (~> 4.3.0) ffaker (~> 2.0.0) fog (~> 1.25.0) font-awesome-rails (~> 4.2) foreman fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) - github-markup + github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-grack (~> 2.0.2) gitlab-linguist (~> 3.0.1) + gitlab_ci_meta (~> 4.0) gitlab_emoji (~> 0.1) gitlab_git (~> 7.2.14) gitlab_meta (= 7.0) - gitlab_omniauth-ldap (= 1.2.1) + gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.0.2) gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) - haml-rails + growl + guard-rspec (~> 4.2.0) + haml-rails (~> 0.5.3) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) - httparty + httparty (~> 0.13.3) jquery-atwho-rails (~> 1.0.0) - jquery-rails (= 3.1.3) - jquery-scrollto-rails - jquery-turbolinks - jquery-ui-rails + jquery-rails (~> 3.1.3) + jquery-scrollto-rails (~> 1.4.3) + jquery-turbolinks (~> 2.0.1) + jquery-ui-rails (~> 4.2.1) kaminari (~> 0.15.1) - letter_opener + letter_opener (~> 1.1.2) mail_room (~> 0.4.0) - minitest (~> 5.3.0) - mousetrap-rails - mysql2 - newrelic_rpm - nprogress-rails - octokit (= 3.7.0) + minitest (~> 5.7.0) + mousetrap-rails (~> 1.4.6) + mysql2 (~> 0.3.16) + nested_form (~> 0.3.2) + newrelic_rpm (~> 3.9.4.245) + nprogress-rails (~> 0.1.2.3) + oauth2 (~> 1.0.0) + octokit (~> 3.7.0) omniauth (~> 1.2.2) - omniauth-bitbucket - omniauth-github - omniauth-gitlab - omniauth-google-oauth2 - omniauth-kerberos + omniauth-bitbucket (~> 0.0.2) + omniauth-github (~> 1.1.1) + omniauth-gitlab (~> 1.0.0) + omniauth-google-oauth2 (~> 0.2.5) + omniauth-kerberos (~> 0.2.0) omniauth-saml (~> 1.4.0) - omniauth-shibboleth - omniauth-twitter - org-ruby (= 0.9.12) - pg + omniauth-shibboleth (~> 1.1.1) + omniauth-twitter (~> 1.0.1) + org-ruby (~> 0.9.12) + paranoia (~> 2.0) + pg (~> 0.18.2) poltergeist (~> 1.6.0) pry-rails - quiet_assets (~> 1.0.1) + quiet_assets (~> 1.0.2) rack-attack (~> 4.3.0) - rack-cors - rack-mini-profiler + rack-cors (~> 0.2.9) + rack-mini-profiler (~> 0.9.0) rack-oauth2 (~> 1.0.5) - rails (= 4.1.11) + rails (= 4.1.12) raphael-rails (~> 2.1.2) + rb-fsevent + rb-inotify rdoc (~> 3.6) redcarpet (~> 3.3.2) - redis-rails - request_store + redis-rails (~> 4.0.0) + request_store (~> 1.2.0) rerun (~> 0.10.0) - rqrcode-rails3 + rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) - rubocop (= 0.28.0) + rubocop (~> 0.28.0) sanitize (~> 2.0) sass-rails (~> 4.0.5) - sdoc - seed-fu + sdoc (~> 0.3.20) + seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) - settingslogic + settingslogic (~> 2.0.9) shoulda-matchers (~> 2.8.0) sidekiq (~> 3.3) - sidetiq (= 0.6.3) - simplecov - sinatra - six + sidetiq (~> 0.6.3) + simplecov (~> 0.10.0) + sinatra (~> 1.4.4) + six (~> 0.2.0) slack-notifier (~> 1.0.0) - slim - spinach-rails - spring (~> 1.3.1) - spring-commands-rspec (~> 1.0.0) + slim (~> 2.0.2) + spinach-rails (~> 0.2.1) + spring (~> 1.3.6) + spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.0.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 2.12.3) - stamp - state_machine - task_list (= 1.0.2) + stamp (~> 0.5.0) + state_machine (~> 1.2.0) + task_list (~> 1.0.2) teaspoon (~> 1.0.0) - teaspoon-jasmine - test_after_commit - thin + teaspoon-jasmine (~> 2.2.0) + test_after_commit (~> 0.2.2) + thin (~> 1.6.1) tinder (~> 1.9.2) turbolinks (~> 2.5.0) - uglifier + uglifier (~> 2.3.2) underscore-rails (~> 1.4.4) - unf - unicorn (~> 4.6.3) - unicorn-worker-killer - version_sorter - virtus + unf (~> 0.1.4) + unicorn (~> 4.8.2) + unicorn-worker-killer (~> 0.4.2) + version_sorter (~> 2.0.0) + virtus (~> 1.0.1) webmock (~> 1.21.0) - wikicloth (= 0.8.1) + whenever (~> 0.8.4) + wikicloth (~> 0.8.1) BUNDLED WITH 1.10.6 diff --git a/Procfile b/Procfile index 18fd9eb3d92..08880b9c425 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q common -q default +worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default # mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e05674e840c6946d5a40f340790289af2dc25f7 GIT binary patch literal 25222 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..2fcb8f2da0d283628878cbc74b9dd848109a4535 GIT binary patch literal 4405 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 AQ~&?~ literal 0 HcmV?d00001 diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png new file mode 100644 index 0000000000000000000000000000000000000000..65d29e3fd891b7b8deba8670cb7d74b03056e3d7 GIT binary patch literal 76024 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 z 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 Vw=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= +Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& +isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? +b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? +0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== +a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* +(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* +a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, +scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", +animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", +scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, +c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, +onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, +pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", +scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); +d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; +h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< +h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= +Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0 + event.preventDefault() + + descr = $(this).closest('.runner-description').first() + descr.addClass('hide') + form = descr.next('.runner-description-form') + descrInput = form.find('input.description') + originalValue = descrInput.val() + form.removeClass('hide') + form.find('.cancel').on 'click', (event) -> + event.preventDefault() + + form.addClass('hide') + descrInput.val(originalValue) + descr.removeClass('hide') + +$(document).on 'click', '.assign-all-runner', -> + $(this).replaceWith(' Assign in progress..') + +window.unbindEvents = -> + $(document).unbind('scroll') + $(document).off('scroll') + +document.addEventListener("page:fetch", unbindEvents) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee new file mode 100644 index 00000000000..be4a3aa757a --- /dev/null +++ b/app/assets/javascripts/ci/build.coffee @@ -0,0 +1,41 @@ +class CiBuild + @interval: null + + constructor: (build_url, build_status) -> + clearInterval(CiBuild.interval) + + if build_status == "running" || build_status == "pending" + # + # Bind autoscroll button to follow build output + # + $("#autoscroll-button").bind "click", -> + state = $(this).data("state") + if "enabled" is state + $(this).data "state", "disabled" + $(this).text "enable autoscroll" + else + $(this).data "state", "enabled" + $(this).text "disable autoscroll" + + # + # Check for new build output if user still watching build page + # Only valid for runnig build when output changes during time + # + CiBuild.interval = setInterval => + if window.location.href is build_url + $.ajax + url: build_url + dataType: "json" + success: (build) => + if build.status == "running" + $('#build-trace code').html build.trace_html + $('#build-trace code').append '' + @checkAutoscroll() + else + Turbolinks.visit build_url + , 4000 + + checkAutoscroll: -> + $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") + +@CiBuild = CiBuild diff --git a/app/assets/javascripts/ci/pager.js.coffee b/app/assets/javascripts/ci/pager.js.coffee new file mode 100644 index 00000000000..b57e7c736e9 --- /dev/null +++ b/app/assets/javascripts/ci/pager.js.coffee @@ -0,0 +1,42 @@ +@CiPager = + init: (@url, @limit = 0, preload, @disable = false) -> + if preload + @offset = 0 + @getItems() + else + @offset = @limit + @initLoadMore() + + getItems: -> + $(".loading").show() + $.ajax + type: "GET" + url: @url + data: "limit=" + @limit + "&offset=" + @offset + complete: => + $(".loading").hide() + success: (data) => + Pager.append(data.count, data.html) + dataType: "json" + + append: (count, html) -> + if count > 1 + $(".content-list").append html + if count == @limit + @offset += count + else + @disable = true + + initLoadMore: -> + $(document).unbind('scroll') + $(document).endlessScroll + bottomPixels: 400 + fireDelay: 1000 + fireOnce: true + ceaseFire: -> + Pager.disable + + callback: (i) => + unless $(".loading").is(':visible') + $(".loading").show() + Pager.getItems() diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee new file mode 100644 index 00000000000..7e028b4e115 --- /dev/null +++ b/app/assets/javascripts/ci/projects.js.coffee @@ -0,0 +1,6 @@ +$(document).on 'click', '.badge-codes-toggle', -> + $('.badge-codes-block').toggleClass("hide") + return false + +$(document).on 'click', '.sync-now', -> + $(this).find('i').addClass('fa-spin') diff --git a/app/assets/stylesheets/ci/application.scss b/app/assets/stylesheets/ci/application.scss new file mode 100644 index 00000000000..ce080c7cf8a --- /dev/null +++ b/app/assets/stylesheets/ci/application.scss @@ -0,0 +1,46 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require_self + */ + +@import "main/variables.scss"; +@import "main/mixins.scss"; +@import "main/fonts.scss"; +@import "main/layout.scss"; + +/** + * Twitter bootstrap + */ +@import 'bootstrap'; + +/** + * Font icons + * + */ +@import "font-awesome"; + +/** + * Generic css (forms, nav etc): + */ +@import "generic/*"; + +/** + * Page specific styles (issues, projects etc): + */ + +@import "sections/*"; + +/* + * NProgress + */ +$nprogress-color: #9BC; +@import 'nprogress'; +@import 'nprogress-bootstrap'; diff --git a/app/assets/stylesheets/ci/generic/avatar.scss b/app/assets/stylesheets/ci/generic/avatar.scss new file mode 100644 index 00000000000..fc0914cddea --- /dev/null +++ b/app/assets/stylesheets/ci/generic/avatar.scss @@ -0,0 +1,29 @@ +.avatar { + float: left; + margin-right: 12px; + width: 40px; + height: 40px; + padding: 0; + @include border-radius($avatar_radius); + + &.avatar-inline { + float: none; + margin-left: 4px; + margin-bottom: 2px; + + &.s16 { margin-right: 4px; } + &.s24 { margin-right: 4px; } + } + + &.avatar-tile { + @include border-radius(0px); + } + + &.s16 { width: 16px; height: 16px; margin-right: 6px; } + &.s24 { width: 24px; height: 24px; margin-right: 8px; } + &.s26 { width: 26px; height: 26px; margin-right: 8px; } + &.s32 { width: 32px; height: 32px; margin-right: 10px; } + &.s60 { width: 60px; height: 60px; margin-right: 12px; } + &.s90 { width: 90px; height: 90px; margin-right: 15px; } + &.s160 { width: 160px; height: 160px; margin-right: 20px; } +} diff --git a/app/assets/stylesheets/ci/generic/buttons.scss b/app/assets/stylesheets/ci/generic/buttons.scss new file mode 100644 index 00000000000..5605c097c03 --- /dev/null +++ b/app/assets/stylesheets/ci/generic/buttons.scss @@ -0,0 +1,7 @@ +.btn { + @extend .btn-default; + + &.btn-save { + @extend .btn-primary; + } +} diff --git a/app/assets/stylesheets/ci/generic/callout.scss b/app/assets/stylesheets/ci/generic/callout.scss new file mode 100644 index 00000000000..f1699d21c9b --- /dev/null +++ b/app/assets/stylesheets/ci/generic/callout.scss @@ -0,0 +1,45 @@ +/* + * Callouts from Bootstrap3 docs + * + * Not quite alerts, but custom and helpful notes for folks reading the docs. + * Requires a base and modifier class. + */ + +/* Common styles for all types */ +.bs-callout { + margin: 20px 0; + padding: 20px; + border-left: 3px solid #eee; + color: #666; + background: #f9f9f9; +} +.bs-callout h4 { + margin-top: 0; + margin-bottom: 5px; +} +.bs-callout p:last-child { + margin-bottom: 0; +} + +/* Variations */ +.bs-callout-danger { + background-color: #fdf7f7; + border-color: #eed3d7; + color: #b94a48; +} +.bs-callout-warning { + background-color: #faf8f0; + border-color: #faebcc; + color: #8a6d3b; +} +.bs-callout-info { + background-color: #f4f8fa; + border-color: #bce8f1; + color: #34789a; +} +.bs-callout-success { + background-color: #dff0d8; + border-color: #5cA64d; + color: #3c763d; +} + diff --git a/app/assets/stylesheets/ci/generic/common.scss b/app/assets/stylesheets/ci/generic/common.scss new file mode 100644 index 00000000000..58b7a93b0ad --- /dev/null +++ b/app/assets/stylesheets/ci/generic/common.scss @@ -0,0 +1,189 @@ +/** COLORS **/ +.cgray { color: gray } +.clgray { color: #BBB } +.cred { color: #D12F19 } +.cgreen { color: #4a2 } +.cblue { color: #29A } +.cblack { color: #111 } +.cdark { color: #444 } +.camber { color: #ffc000 } +.cwhite { color: #fff!important } +.bgred { background: #F2DEDE!important } + +/** COMMON CLASSES **/ +.prepend-top-10 { margin-top:10px } +.prepend-top-20 { margin-top:20px } +.prepend-left-10 { margin-left:10px } +.prepend-left-20 { margin-left:20px } +.append-right-10 { margin-right:10px } +.append-right-20 { margin-right:20px } +.append-bottom-10 { margin-bottom:10px } +.append-bottom-15 { margin-bottom:15px } +.append-bottom-20 { margin-bottom:20px } +.inline { display: inline-block } +.padded { padding:20px } +.ipadded { padding:20px!important } +.lborder { border-left:1px solid #eee } +.underlined_link { text-decoration: underline; } +.hint { font-style: italic; color: #999; } +.light { color: #888 } +.tiny { font-weight: normal } +.vtop { vertical-align: top !important; } + + +.dropdown-menu > li > a { + text-shadow: none; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + background: #29b; +} + +.breadcrumb > li + li:before { + content: "/"; + padding: 0; + color: #666; +} + +.str-truncated { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + max-width: 82%; +} + +.page-title { + color: #444; + line-height: 1.5; + margin-top: 0px; + margin-bottom: 15px; +} + +.slead { + margin-bottom: 18px; + font-size: 16px; + font-weight: normal; + line-height: 1.4; +} + +.help-callout { + li { + font-size: 15px; + line-height: 1.6; + } +} + +/** light list with border-bottom between li **/ +ul.bordered-list { + margin: 5px 0px; + padding: 0px; + li { + padding: 5px 0; + border-bottom: 1px solid #EEE; + overflow: hidden; + display: block; + margin: 0px; + &:last-child { border:none } + &.active { + background: #f9f9f9; + a { font-weight: bold; } + } + } + + &.top-list { + li:first-child { + padding-top: 0; + h4, h5 { + margin-top: 0; + } + } + } +} + +.underlined-title { + border-bottom: 1px solid #ccc; + padding: 0 0 3px 3px; +} + +// Nav tabs +.nav.nav-tabs { + li { + > a { + padding: 8px 20px; + margin-right: 7px; + line-height: 20px; + border-color: #EEE; + color: #888; + border-bottom: 1px solid #ddd; + .badge { + background-color: #eee; + color: #888; + text-shadow: 0 1px 1px #fff; + } + i[class^="fa-"] { + line-height: 14px; + } + } + &.active { + > a { + border-color: #CCC; + border-bottom: 1px solid #fff; + color: #333; + font-weight: bold; + } + } + } + + &.nav-small-tabs > li > a { + padding: 6px 9px; + } +} + +.nav-tabs > li > a, +.nav-pills > li > a { + color: #666; +} + +.nav-small > li > a { + padding: 3px 5px; + font-size: 12px; +} + + + +// Breadcrumb +ul.breadcrumb { + background: white; + border: none; + li { + display: inline; + text-shadow: 0 1px 0 white + } + + a { + font-size: 16px; + } +} + +/** + * fix to keep tooltips position in top navigation bar + * + */ +.navbar .nav > li { + position: relative; + white-space: nowrap; +} + +// alerts +.alert-disabled { + background-color: #e6e6e6; + border-color: #ebccd1; + color: #b0b0b0; +} + +.label { + margin-right: 5px; + font-weight: normal; +} diff --git a/app/assets/stylesheets/ci/generic/forms.scss b/app/assets/stylesheets/ci/generic/forms.scss new file mode 100644 index 00000000000..c8e4e8d6602 --- /dev/null +++ b/app/assets/stylesheets/ci/generic/forms.scss @@ -0,0 +1,28 @@ +input[type='text'].danger { + background: #F2DEDE!important; + border-color: #D66; + text-shadow: 0 1px 1px #fff +} + +fieldset { + margin-bottom: 25px; +} + +.form-actions { + padding: 17px 20px 18px; + margin-top: 18px; + margin-bottom: 18px; + background-color: whitesmoke; + border-top: 1px solid #e5e5e5; + padding-left: 17%; +} + +label { + &.control-label { + @extend .col-sm-2; + } + + &.inline-label { + margin: 0; + } +} diff --git a/app/assets/stylesheets/ci/generic/tables.scss b/app/assets/stylesheets/ci/generic/tables.scss new file mode 100644 index 00000000000..71a7d4abaee --- /dev/null +++ b/app/assets/stylesheets/ci/generic/tables.scss @@ -0,0 +1,20 @@ +table { + &.table { + tr { + td, th { + padding: 8px 10px; + line-height: 20px; + vertical-align: middle; + } + th { + font-weight: normal; + font-size: 15px; + border-bottom: 1px solid #CCC !important; + } + td { + border-color: #F1F1F1 !important; + border-bottom: 1px solid; + } + } + } +} diff --git a/app/assets/stylesheets/ci/generic/typography.scss b/app/assets/stylesheets/ci/generic/typography.scss new file mode 100644 index 00000000000..b9ed23b9d3a --- /dev/null +++ b/app/assets/stylesheets/ci/generic/typography.scss @@ -0,0 +1,63 @@ +h6 { + color: #888; + text-transform: uppercase; +} + +pre { + font-family: $monospace_font; + + &.dark { + background: #333; + color: #f5f5f5; + } +} + +/** + * Links + * + */ +a { + outline: none; + color: $link_color; + &:hover { + text-decoration: none; + color: $primary_color; + } + + &:focus { + text-decoration: underline; + } + + &.dark { + color: $style_color; + } + + &.lined { + text-decoration: underline; + &:hover { text-decoration: underline; } + } + + &.gray { + color: gray; + } + + &.supp_diff_link { + text-align: center; + padding: 20px 0; + background: #f1f1f1; + width: 100%; + float: left; + } + + &.neib { + margin-right: 15px; + } +} + +a:focus { + outline: none; +} + +.monospace { + font-family: $monospace_font; +} diff --git a/app/assets/stylesheets/ci/generic/xterm.scss b/app/assets/stylesheets/ci/generic/xterm.scss new file mode 100644 index 00000000000..460a6bb2024 --- /dev/null +++ b/app/assets/stylesheets/ci/generic/xterm.scss @@ -0,0 +1,904 @@ +// color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg +// see also: https://gist.github.com/jasonm23/2868981 + +$black: #000000; +$red: #cd0000; +$green: #00cd00; +$yellow: #cdcd00; +$blue: #0000ee; // according to wikipedia, this is the xterm standard +//$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile) +$magenta: #cd00cd; +$cyan: #00cdcd; +$white: #e5e5e5; +$l-black: #7f7f7f; +$l-red: #ff0000; +$l-green: #00ff00; +$l-yellow: #ffff00; +$l-blue: #5c5cff; +$l-magenta: #ff00ff; +$l-cyan: #00ffff; +$l-white: #ffffff; + +.term-bold { + font-weight: bold; +} +.term-italic { + font-style: italic; +} +.term-conceal { + visibility: hidden; +} +.term-underline { + text-decoration: underline; +} +.term-cross { + text-decoration: line-through; +} + +.term-fg-black { + color: $black; +} +.term-fg-red { + color: $red; +} +.term-fg-green { + color: $green; +} +.term-fg-yellow { + color: $yellow; +} +.term-fg-blue { + color: $blue; +} +.term-fg-magenta { + color: $magenta; +} +.term-fg-cyan { + color: $cyan; +} +.term-fg-white { + color: $white; +} +.term-fg-l-black { + color: $l-black; +} +.term-fg-l-red { + color: $l-red; +} +.term-fg-l-green { + color: $l-green; +} +.term-fg-l-yellow { + color: $l-yellow; +} +.term-fg-l-blue { + color: $l-blue; +} +.term-fg-l-magenta { + color: $l-magenta; +} +.term-fg-l-cyan { + color: $l-cyan; +} +.term-fg-l-white { + color: $l-white; +} + +.term-bg-black { + background-color: $black; +} +.term-bg-red { + background-color: $red; +} +.term-bg-green { + background-color: $green; +} +.term-bg-yellow { + background-color: $yellow; +} +.term-bg-blue { + background-color: $blue; +} +.term-bg-magenta { + background-color: $magenta; +} +.term-bg-cyan { + background-color: $cyan; +} +.term-bg-white { + background-color: $white; +} +.term-bg-l-black { + background-color: $l-black; +} +.term-bg-l-red { + background-color: $l-red; +} +.term-bg-l-green { + background-color: $l-green; +} +.term-bg-l-yellow { + background-color: $l-yellow; +} +.term-bg-l-blue { + background-color: $l-blue; +} +.term-bg-l-magenta { + background-color: $l-magenta; +} +.term-bg-l-cyan { + background-color: $l-cyan; +} +.term-bg-l-white { + background-color: $l-white; +} + + +.xterm-fg-0 { + color: #000000; +} +.xterm-fg-1 { + color: #800000; +} +.xterm-fg-2 { + color: #008000; +} +.xterm-fg-3 { + color: #808000; +} +.xterm-fg-4 { + color: #000080; +} +.xterm-fg-5 { + color: #800080; +} +.xterm-fg-6 { + color: #008080; +} +.xterm-fg-7 { + color: #c0c0c0; +} +.xterm-fg-8 { + color: #808080; +} +.xterm-fg-9 { + color: #ff0000; +} +.xterm-fg-10 { + color: #00ff00; +} +.xterm-fg-11 { + color: #ffff00; +} +.xterm-fg-12 { + color: #0000ff; +} +.xterm-fg-13 { + color: #ff00ff; +} +.xterm-fg-14 { + color: #00ffff; +} +.xterm-fg-15 { + color: #ffffff; +} +.xterm-fg-16 { + color: #000000; +} +.xterm-fg-17 { + color: #00005f; +} +.xterm-fg-18 { + color: #000087; +} +.xterm-fg-19 { + color: #0000af; +} +.xterm-fg-20 { + color: #0000d7; +} +.xterm-fg-21 { + color: #0000ff; +} +.xterm-fg-22 { + color: #005f00; +} +.xterm-fg-23 { + color: #005f5f; +} +.xterm-fg-24 { + color: #005f87; +} +.xterm-fg-25 { + color: #005faf; +} +.xterm-fg-26 { + color: #005fd7; +} +.xterm-fg-27 { + color: #005fff; +} +.xterm-fg-28 { + color: #008700; +} +.xterm-fg-29 { + color: #00875f; +} +.xterm-fg-30 { + color: #008787; +} +.xterm-fg-31 { + color: #0087af; +} +.xterm-fg-32 { + color: #0087d7; +} +.xterm-fg-33 { + color: #0087ff; +} +.xterm-fg-34 { + color: #00af00; +} +.xterm-fg-35 { + color: #00af5f; +} +.xterm-fg-36 { + color: #00af87; +} +.xterm-fg-37 { + color: #00afaf; +} +.xterm-fg-38 { + color: #00afd7; +} +.xterm-fg-39 { + color: #00afff; +} +.xterm-fg-40 { + color: #00d700; +} +.xterm-fg-41 { + color: #00d75f; +} +.xterm-fg-42 { + color: #00d787; +} +.xterm-fg-43 { + color: #00d7af; +} +.xterm-fg-44 { + color: #00d7d7; +} +.xterm-fg-45 { + color: #00d7ff; +} +.xterm-fg-46 { + color: #00ff00; +} +.xterm-fg-47 { + color: #00ff5f; +} +.xterm-fg-48 { + color: #00ff87; +} +.xterm-fg-49 { + color: #00ffaf; +} +.xterm-fg-50 { + color: #00ffd7; +} +.xterm-fg-51 { + color: #00ffff; +} +.xterm-fg-52 { + color: #5f0000; +} +.xterm-fg-53 { + color: #5f005f; +} +.xterm-fg-54 { + color: #5f0087; +} +.xterm-fg-55 { + color: #5f00af; +} +.xterm-fg-56 { + color: #5f00d7; +} +.xterm-fg-57 { + color: #5f00ff; +} +.xterm-fg-58 { + color: #5f5f00; +} +.xterm-fg-59 { + color: #5f5f5f; +} +.xterm-fg-60 { + color: #5f5f87; +} +.xterm-fg-61 { + color: #5f5faf; +} +.xterm-fg-62 { + color: #5f5fd7; +} +.xterm-fg-63 { + color: #5f5fff; +} +.xterm-fg-64 { + color: #5f8700; +} +.xterm-fg-65 { + color: #5f875f; +} +.xterm-fg-66 { + color: #5f8787; +} +.xterm-fg-67 { + color: #5f87af; +} +.xterm-fg-68 { + color: #5f87d7; +} +.xterm-fg-69 { + color: #5f87ff; +} +.xterm-fg-70 { + color: #5faf00; +} +.xterm-fg-71 { + color: #5faf5f; +} +.xterm-fg-72 { + color: #5faf87; +} +.xterm-fg-73 { + color: #5fafaf; +} +.xterm-fg-74 { + color: #5fafd7; +} +.xterm-fg-75 { + color: #5fafff; +} +.xterm-fg-76 { + color: #5fd700; +} +.xterm-fg-77 { + color: #5fd75f; +} +.xterm-fg-78 { + color: #5fd787; +} +.xterm-fg-79 { + color: #5fd7af; +} +.xterm-fg-80 { + color: #5fd7d7; +} +.xterm-fg-81 { + color: #5fd7ff; +} +.xterm-fg-82 { + color: #5fff00; +} +.xterm-fg-83 { + color: #5fff5f; +} +.xterm-fg-84 { + color: #5fff87; +} +.xterm-fg-85 { + color: #5fffaf; +} +.xterm-fg-86 { + color: #5fffd7; +} +.xterm-fg-87 { + color: #5fffff; +} +.xterm-fg-88 { + color: #870000; +} +.xterm-fg-89 { + color: #87005f; +} +.xterm-fg-90 { + color: #870087; +} +.xterm-fg-91 { + color: #8700af; +} +.xterm-fg-92 { + color: #8700d7; +} +.xterm-fg-93 { + color: #8700ff; +} +.xterm-fg-94 { + color: #875f00; +} +.xterm-fg-95 { + color: #875f5f; +} +.xterm-fg-96 { + color: #875f87; +} +.xterm-fg-97 { + color: #875faf; +} +.xterm-fg-98 { + color: #875fd7; +} +.xterm-fg-99 { + color: #875fff; +} +.xterm-fg-100 { + color: #878700; +} +.xterm-fg-101 { + color: #87875f; +} +.xterm-fg-102 { + color: #878787; +} +.xterm-fg-103 { + color: #8787af; +} +.xterm-fg-104 { + color: #8787d7; +} +.xterm-fg-105 { + color: #8787ff; +} +.xterm-fg-106 { + color: #87af00; +} +.xterm-fg-107 { + color: #87af5f; +} +.xterm-fg-108 { + color: #87af87; +} +.xterm-fg-109 { + color: #87afaf; +} +.xterm-fg-110 { + color: #87afd7; +} +.xterm-fg-111 { + color: #87afff; +} +.xterm-fg-112 { + color: #87d700; +} +.xterm-fg-113 { + color: #87d75f; +} +.xterm-fg-114 { + color: #87d787; +} +.xterm-fg-115 { + color: #87d7af; +} +.xterm-fg-116 { + color: #87d7d7; +} +.xterm-fg-117 { + color: #87d7ff; +} +.xterm-fg-118 { + color: #87ff00; +} +.xterm-fg-119 { + color: #87ff5f; +} +.xterm-fg-120 { + color: #87ff87; +} +.xterm-fg-121 { + color: #87ffaf; +} +.xterm-fg-122 { + color: #87ffd7; +} +.xterm-fg-123 { + color: #87ffff; +} +.xterm-fg-124 { + color: #af0000; +} +.xterm-fg-125 { + color: #af005f; +} +.xterm-fg-126 { + color: #af0087; +} +.xterm-fg-127 { + color: #af00af; +} +.xterm-fg-128 { + color: #af00d7; +} +.xterm-fg-129 { + color: #af00ff; +} +.xterm-fg-130 { + color: #af5f00; +} +.xterm-fg-131 { + color: #af5f5f; +} +.xterm-fg-132 { + color: #af5f87; +} +.xterm-fg-133 { + color: #af5faf; +} +.xterm-fg-134 { + color: #af5fd7; +} +.xterm-fg-135 { + color: #af5fff; +} +.xterm-fg-136 { + color: #af8700; +} +.xterm-fg-137 { + color: #af875f; +} +.xterm-fg-138 { + color: #af8787; +} +.xterm-fg-139 { + color: #af87af; +} +.xterm-fg-140 { + color: #af87d7; +} +.xterm-fg-141 { + color: #af87ff; +} +.xterm-fg-142 { + color: #afaf00; +} +.xterm-fg-143 { + color: #afaf5f; +} +.xterm-fg-144 { + color: #afaf87; +} +.xterm-fg-145 { + color: #afafaf; +} +.xterm-fg-146 { + color: #afafd7; +} +.xterm-fg-147 { + color: #afafff; +} +.xterm-fg-148 { + color: #afd700; +} +.xterm-fg-149 { + color: #afd75f; +} +.xterm-fg-150 { + color: #afd787; +} +.xterm-fg-151 { + color: #afd7af; +} +.xterm-fg-152 { + color: #afd7d7; +} +.xterm-fg-153 { + color: #afd7ff; +} +.xterm-fg-154 { + color: #afff00; +} +.xterm-fg-155 { + color: #afff5f; +} +.xterm-fg-156 { + color: #afff87; +} +.xterm-fg-157 { + color: #afffaf; +} +.xterm-fg-158 { + color: #afffd7; +} +.xterm-fg-159 { + color: #afffff; +} +.xterm-fg-160 { + color: #d70000; +} +.xterm-fg-161 { + color: #d7005f; +} +.xterm-fg-162 { + color: #d70087; +} +.xterm-fg-163 { + color: #d700af; +} +.xterm-fg-164 { + color: #d700d7; +} +.xterm-fg-165 { + color: #d700ff; +} +.xterm-fg-166 { + color: #d75f00; +} +.xterm-fg-167 { + color: #d75f5f; +} +.xterm-fg-168 { + color: #d75f87; +} +.xterm-fg-169 { + color: #d75faf; +} +.xterm-fg-170 { + color: #d75fd7; +} +.xterm-fg-171 { + color: #d75fff; +} +.xterm-fg-172 { + color: #d78700; +} +.xterm-fg-173 { + color: #d7875f; +} +.xterm-fg-174 { + color: #d78787; +} +.xterm-fg-175 { + color: #d787af; +} +.xterm-fg-176 { + color: #d787d7; +} +.xterm-fg-177 { + color: #d787ff; +} +.xterm-fg-178 { + color: #d7af00; +} +.xterm-fg-179 { + color: #d7af5f; +} +.xterm-fg-180 { + color: #d7af87; +} +.xterm-fg-181 { + color: #d7afaf; +} +.xterm-fg-182 { + color: #d7afd7; +} +.xterm-fg-183 { + color: #d7afff; +} +.xterm-fg-184 { + color: #d7d700; +} +.xterm-fg-185 { + color: #d7d75f; +} +.xterm-fg-186 { + color: #d7d787; +} +.xterm-fg-187 { + color: #d7d7af; +} +.xterm-fg-188 { + color: #d7d7d7; +} +.xterm-fg-189 { + color: #d7d7ff; +} +.xterm-fg-190 { + color: #d7ff00; +} +.xterm-fg-191 { + color: #d7ff5f; +} +.xterm-fg-192 { + color: #d7ff87; +} +.xterm-fg-193 { + color: #d7ffaf; +} +.xterm-fg-194 { + color: #d7ffd7; +} +.xterm-fg-195 { + color: #d7ffff; +} +.xterm-fg-196 { + color: #ff0000; +} +.xterm-fg-197 { + color: #ff005f; +} +.xterm-fg-198 { + color: #ff0087; +} +.xterm-fg-199 { + color: #ff00af; +} +.xterm-fg-200 { + color: #ff00d7; +} +.xterm-fg-201 { + color: #ff00ff; +} +.xterm-fg-202 { + color: #ff5f00; +} +.xterm-fg-203 { + color: #ff5f5f; +} +.xterm-fg-204 { + color: #ff5f87; +} +.xterm-fg-205 { + color: #ff5faf; +} +.xterm-fg-206 { + color: #ff5fd7; +} +.xterm-fg-207 { + color: #ff5fff; +} +.xterm-fg-208 { + color: #ff8700; +} +.xterm-fg-209 { + color: #ff875f; +} +.xterm-fg-210 { + color: #ff8787; +} +.xterm-fg-211 { + color: #ff87af; +} +.xterm-fg-212 { + color: #ff87d7; +} +.xterm-fg-213 { + color: #ff87ff; +} +.xterm-fg-214 { + color: #ffaf00; +} +.xterm-fg-215 { + color: #ffaf5f; +} +.xterm-fg-216 { + color: #ffaf87; +} +.xterm-fg-217 { + color: #ffafaf; +} +.xterm-fg-218 { + color: #ffafd7; +} +.xterm-fg-219 { + color: #ffafff; +} +.xterm-fg-220 { + color: #ffd700; +} +.xterm-fg-221 { + color: #ffd75f; +} +.xterm-fg-222 { + color: #ffd787; +} +.xterm-fg-223 { + color: #ffd7af; +} +.xterm-fg-224 { + color: #ffd7d7; +} +.xterm-fg-225 { + color: #ffd7ff; +} +.xterm-fg-226 { + color: #ffff00; +} +.xterm-fg-227 { + color: #ffff5f; +} +.xterm-fg-228 { + color: #ffff87; +} +.xterm-fg-229 { + color: #ffffaf; +} +.xterm-fg-230 { + color: #ffffd7; +} +.xterm-fg-231 { + color: #ffffff; +} +.xterm-fg-232 { + color: #080808; +} +.xterm-fg-233 { + color: #121212; +} +.xterm-fg-234 { + color: #1c1c1c; +} +.xterm-fg-235 { + color: #262626; +} +.xterm-fg-236 { + color: #303030; +} +.xterm-fg-237 { + color: #3a3a3a; +} +.xterm-fg-238 { + color: #444444; +} +.xterm-fg-239 { + color: #4e4e4e; +} +.xterm-fg-240 { + color: #585858; +} +.xterm-fg-241 { + color: #626262; +} +.xterm-fg-242 { + color: #6c6c6c; +} +.xterm-fg-243 { + color: #767676; +} +.xterm-fg-244 { + color: #808080; +} +.xterm-fg-245 { + color: #8a8a8a; +} +.xterm-fg-246 { + color: #949494; +} +.xterm-fg-247 { + color: #9e9e9e; +} +.xterm-fg-248 { + color: #a8a8a8; +} +.xterm-fg-249 { + color: #b2b2b2; +} +.xterm-fg-250 { + color: #bcbcbc; +} +.xterm-fg-251 { + color: #c6c6c6; +} +.xterm-fg-252 { + color: #d0d0d0; +} +.xterm-fg-253 { + color: #dadada; +} +.xterm-fg-254 { + color: #e4e4e4; +} +.xterm-fg-255 { + color: #eeeeee; +} diff --git a/app/assets/stylesheets/ci/main/fonts.scss b/app/assets/stylesheets/ci/main/fonts.scss new file mode 100644 index 00000000000..8cc9986415c --- /dev/null +++ b/app/assets/stylesheets/ci/main/fonts.scss @@ -0,0 +1,2 @@ +/** Typo **/ +$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'Courier New', 'andale mono', 'lucida console', monospace; diff --git a/app/assets/stylesheets/ci/main/layout.scss b/app/assets/stylesheets/ci/main/layout.scss new file mode 100644 index 00000000000..fa54481fa05 --- /dev/null +++ b/app/assets/stylesheets/ci/main/layout.scss @@ -0,0 +1,18 @@ +html { + overflow-y: scroll; + + &.touch .tooltip { display: none !important; } +} + +body { + margin-bottom: 20px; +} + +.container { + padding-top: 0; + z-index: 5; +} + +.container .content { + margin: 0 0; +} diff --git a/app/assets/stylesheets/ci/main/mixins.scss b/app/assets/stylesheets/ci/main/mixins.scss new file mode 100644 index 00000000000..40040822331 --- /dev/null +++ b/app/assets/stylesheets/ci/main/mixins.scss @@ -0,0 +1,31 @@ +@mixin box-shadow($shadow) { + -webkit-box-shadow: $shadow; + -moz-box-shadow: $shadow; + -ms-box-shadow: $shadow; + -o-box-shadow: $shadow; + box-shadow: $shadow; +} + +@mixin border-radius($radius) { + -webkit-border-radius: $radius; + -moz-border-radius: $radius; + -ms-border-radius: $radius; + -o-border-radius: $radius; + border-radius: $radius; +} + +@mixin linear-gradient($from, $to) { + background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to)); + background-image: -webkit-linear-gradient($from, $to); + background-image: -moz-linear-gradient($from, $to); + background-image: -ms-linear-gradient($from, $to); + background-image: -o-linear-gradient($from, $to); +} + +@mixin transition($transition) { + -webkit-transition: $transition; + -moz-transition: $transition; + -ms-transition: $transition; + -o-transition: $transition; + transition: $transition; +} diff --git a/app/assets/stylesheets/ci/main/variables.scss b/app/assets/stylesheets/ci/main/variables.scss new file mode 100644 index 00000000000..a8c672a8057 --- /dev/null +++ b/app/assets/stylesheets/ci/main/variables.scss @@ -0,0 +1,44 @@ +/** + * General Colors + */ +$primary_color: #2FA0BB; +$link_color: #3A89A3; +$style_color: #246; +$bg_style_color: #246; +$hover: #D9EDF7; + +/* + * Success colors (green) + */ +$border_success: #019875; +$bg_success: #019875; + +/* + * Danger colors (red) + */ +$border_danger: #d43f3a; +$bg_danger: #d9534f; + +/* + * Primary colors (blue) + */ +$border_primary: #246; +$bg_primary: #246; + +/* + * Warning colors (yellow) + */ +$bg_warning: #EB9532; +$border_warning: #EB9532; + +/** + * Twitter bootstrap variables + */ +$font-size-base: 13px !default; +$nav-pills-active-link-hover-bg: $bg_style_color; +$pagination-active-bg: $bg_style_color; + +/** + * Avatar variables + */ +$avatar_radius: 50%; diff --git a/app/assets/stylesheets/ci/sections/builds.scss b/app/assets/stylesheets/ci/sections/builds.scss new file mode 100644 index 00000000000..a9d39bb0cbd --- /dev/null +++ b/app/assets/stylesheets/ci/sections/builds.scss @@ -0,0 +1,54 @@ +pre.trace { + background: #111111; + color: #fff; + font-family: $monospace_font; + white-space: pre; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + overflow: auto; + overflow-y: hidden; + font-size: 12px; + + .fa-refresh { + font-size: 24px; + margin-left: 20px; + } +} + +.autoscroll-container { + position: fixed; + bottom: 10px; + right: 20px; + z-index: 100; +} + +.scroll-controls { + position: fixed; + bottom: 10px; + left: 20px; + z-index: 100; + + a { + display: block; + margin-bottom: 5px; + } +} + +.build-widget { + padding: 10px; + background: #f4f4f4; + margin-bottom: 20px; + border-radius: 4px; + + .title { + margin-top: 0; + color: #666; + line-height: 1.5; + } + .attr-name { + color: #777; + } +} diff --git a/app/assets/stylesheets/ci/sections/lint.scss b/app/assets/stylesheets/ci/sections/lint.scss new file mode 100644 index 00000000000..7191b5d47aa --- /dev/null +++ b/app/assets/stylesheets/ci/sections/lint.scss @@ -0,0 +1,8 @@ +.incorrect-syntax{ + font-size: 19px; + color: red; +} +.correct-syntax{ + font-size: 19px; + color: #47a447; +} \ No newline at end of file diff --git a/app/assets/stylesheets/ci/sections/login.scss b/app/assets/stylesheets/ci/sections/login.scss new file mode 100644 index 00000000000..47e453ec8d2 --- /dev/null +++ b/app/assets/stylesheets/ci/sections/login.scss @@ -0,0 +1,13 @@ +.login-block { + padding: 15px; + margin: 0 auto; + text-align: center; + + p { + font-size: 15px; + } + + .btn-login { + padding: 18px 32px; + } +} diff --git a/app/assets/stylesheets/ci/sections/navbar.scss b/app/assets/stylesheets/ci/sections/navbar.scss new file mode 100644 index 00000000000..efa70eb2956 --- /dev/null +++ b/app/assets/stylesheets/ci/sections/navbar.scss @@ -0,0 +1,54 @@ +.navbar-static-top { + margin-bottom: 20px; +} + +.navbar-ci { + background: $style_color; + + .navbar-brand { + color: #fff; + + &:hover { + color: #fff; + } + } + .brand, + .nav > li > a { + color: #fff; + + &:hover, &:focus, &:active { + background: none; + } + } + + .profile-holder { + position: relative; + + img { + position: absolute; + top: -8px; + width: 32px; + @include border-radius(32px); + } + + span { + margin-left: 42px; + } + } + + .btn-login { + padding: 7px 22px; + margin-top: 7px; + &:hover, &:active, &:focus { + background: #018865 !important; + } + } +} + +.turbolink-spinner { + position: absolute; + top: 11px; + left: 50%; + color: #FFF; + font-size: 20px; +} diff --git a/app/assets/stylesheets/ci/sections/projects.scss b/app/assets/stylesheets/ci/sections/projects.scss new file mode 100644 index 00000000000..84ee1399bff --- /dev/null +++ b/app/assets/stylesheets/ci/sections/projects.scss @@ -0,0 +1,61 @@ +.project-title { + margin: 0; + color: #444; + font-size: 20px; + line-height: 1.5; +} + +.builds { + @extend .table; + + .build { + &.alert{ + margin-bottom: 6px; + } + } +} + +.projects-table { + td { + vertical-align: middle !important; + } +} + +.commit-info { + font-size: 14px; + + .attr-name { + font-weight: 300; + color: #666; + margin-right: 5px; + } + + pre.commit-message { + font-size: 14px; + background: none; + padding: 0; + margin: 0; + border: none; + margin: 20px 0; + border-bottom: 1px solid #EEE; + padding-bottom: 20px; + border-radius: 0; + } +} + +.search{ + width: 300px; + + .search-input{ + height: 35px; + } + + form{ + margin-top: 0; + margin-bottom: 0; + } +} + +.loading{ + font-size: 20px; +} diff --git a/app/assets/stylesheets/ci/sections/runners.scss b/app/assets/stylesheets/ci/sections/runners.scss new file mode 100644 index 00000000000..a9111a7388f --- /dev/null +++ b/app/assets/stylesheets/ci/sections/runners.scss @@ -0,0 +1,34 @@ +.runner-state { + padding: 6px 12px; + margin-right: 10px; + color: #FFF; + + &.runner-state-shared { + background: #32b186; + } + &.runner-state-specific { + background: #3498db; + } +} + +.runner-status-online { + color: green; +} + +.runner-status-offline { + color: gray; +} + +.runner-status-paused { + color: red; +} + +.runner { + .btn { + padding: 1px 6px; + } + + h4 { + font-weight: normal; + } +} diff --git a/app/assets/stylesheets/ci/sections/setup.scss b/app/assets/stylesheets/ci/sections/setup.scss new file mode 100644 index 00000000000..242614616d1 --- /dev/null +++ b/app/assets/stylesheets/ci/sections/setup.scss @@ -0,0 +1,11 @@ +.welcome-block { + margin-top: 50px; + color: #555; + font-size: 16px; + line-height: 1.5; + + h1, h2, h3 { + font-weight: bold; + margin-bottom: 20px; + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 12d439b0b31..ac9484a4cd2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,9 +1,13 @@ require 'gon' class ApplicationController < ActionController::Base + def self.railtie_helpers_paths + "app/helpers/gitlab" + end + include Gitlab::CurrentSettings - include GitlabRoutingHelper - include PageLayoutHelper + include Gitlab::GitlabRoutingHelper + include Gitlab::PageLayoutHelper PER_PAGE = 20 @@ -131,9 +135,6 @@ def project def repository @repository ||= project.repository - rescue Grit::NoSuchPathError => e - log_exception(e) - nil end def authorize_project!(action) diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb new file mode 100644 index 00000000000..430fae14c7d --- /dev/null +++ b/app/controllers/ci/admin/application_controller.rb @@ -0,0 +1,10 @@ +module Ci + module Admin + class ApplicationController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :authenticate_admin! + + layout "ci/admin" + end + end +end diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb new file mode 100644 index 00000000000..71e253fac67 --- /dev/null +++ b/app/controllers/ci/admin/application_settings_controller.rb @@ -0,0 +1,31 @@ +module Ci + class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController + before_action :set_application_setting + + def show + end + + def update + if @application_setting.update_attributes(application_setting_params) + redirect_to ci_admin_application_settings_path, + notice: 'Application settings saved successfully' + else + render :show + end + end + + private + + def set_application_setting + @application_setting = Ci::ApplicationSetting.current + @application_setting ||= Ci::ApplicationSetting.create_from_defaults + end + + def application_setting_params + params.require(:application_setting).permit( + :all_broken_builds, + :add_pusher, + ) + end + end +end diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb new file mode 100644 index 00000000000..8fc776dd98e --- /dev/null +++ b/app/controllers/ci/admin/builds_controller.rb @@ -0,0 +1,12 @@ +module Ci + class Admin::BuildsController < Ci::Admin::ApplicationController + def index + @scope = params[:scope] + @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30) + + if ["pending", "running"].include? @scope + @builds = @builds.send(@scope) + end + end + end +end diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb new file mode 100644 index 00000000000..5939efff980 --- /dev/null +++ b/app/controllers/ci/admin/events_controller.rb @@ -0,0 +1,9 @@ +module Ci + class Admin::EventsController < Ci::Admin::ApplicationController + EVENTS_PER_PAGE = 50 + + def index + @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE) + end + end +end diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb new file mode 100644 index 00000000000..5bbd0ce7396 --- /dev/null +++ b/app/controllers/ci/admin/projects_controller.rb @@ -0,0 +1,19 @@ +module Ci + class Admin::ProjectsController < Ci::Admin::ApplicationController + def index + @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30) + end + + def destroy + project.destroy + + redirect_to ci_projects_url + end + + protected + + def project + @project ||= Ci::Project.find(params[:id]) + end + end +end diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb new file mode 100644 index 00000000000..e7de6eb12ca --- /dev/null +++ b/app/controllers/ci/admin/runner_projects_controller.rb @@ -0,0 +1,34 @@ +module Ci + class Admin::RunnerProjectsController < Ci::Admin::ApplicationController + layout 'ci/project' + + 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) + redirect_to ci_admin_runner_path(@runner) + else + redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project' + end + end + + def destroy + rp = Ci::RunnerProject.find(params[:id]) + runner = rp.runner + rp.destroy + + redirect_to ci_admin_runner_path(runner) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb new file mode 100644 index 00000000000..4f5f3776ddc --- /dev/null +++ b/app/controllers/ci/admin/runners_controller.rb @@ -0,0 +1,69 @@ +module Ci + class Admin::RunnersController < Ci::Admin::ApplicationController + before_filter :runner, except: :index + + def index + @runners = Ci::Runner.order('id DESC') + @runners = @runners.search(params[:search]) if params[:search].present? + @runners = @runners.page(params[:page]).per(30) + @active_runners_cnt = Ci::Runner.where("contacted_at > ?", 1.minutes.ago).count + end + + def show + @builds = @runner.builds.order('id DESC').first(30) + @projects = Ci::Project.all + @projects = @projects.search(params[:search]) if params[:search].present? + @projects = @projects.where("projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any? + @projects = @projects.page(params[:page]).per(30) + end + + def update + @runner.update_attributes(runner_params) + + respond_to do |format| + format.js + format.html { redirect_to ci_admin_runner_path(@runner) } + end + end + + def destroy + @runner.destroy + + redirect_to ci_admin_runners_path + end + + def resume + if @runner.update_attributes(active: true) + redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.' + else + redirect_to ci_admin_runners_path, alert: 'Runner was not updated.' + end + end + + def assign_all + Ci::Project.unassigned(@runner).all.each do |project| + @runner.assign_to(project, current_user) + end + + redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects" + end + + private + + def runner + @runner ||= Ci::Runner.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:token, :description, :tag_list, :contacted_at, :active) + end + end +end diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb new file mode 100644 index 00000000000..726781cb30b --- /dev/null +++ b/app/controllers/ci/application_controller.rb @@ -0,0 +1,133 @@ +module Ci + class ApplicationController < ActionController::Base + def self.railtie_helpers_paths + "app/helpers/ci" + end + + include Ci::UserSessionsHelper + + rescue_from Ci::Network::UnauthorizedError, with: :invalid_token + before_filter :default_headers + before_filter :check_config + + protect_from_forgery + + helper_method :current_user + before_filter :reset_cache + + private + + def current_user + @current_user ||= session[:ci_current_user] + end + + def sign_in(user) + session[:ci_current_user] = user + end + + def sign_out + reset_session + end + + def authenticate_user! + unless current_user + redirect_to new_ci_user_sessions_path + return + end + end + + def authenticate_admin! + unless current_user && current_user.is_admin + redirect_to new_ci_user_sessions_path + return + end + end + + def authenticate_public_page! + unless project.public + unless current_user + redirect_to(new_ci_user_sessions_path(state: generate_oauth_state(request.fullpath))) and return + end + + unless current_user.can_access_project?(project.gitlab_id) + page_404 and return + end + end + end + + def authenticate_token! + unless project.valid_token?(params[:token]) + return head(403) + end + end + + def authorize_access_project! + unless current_user.can_access_project?(@project.gitlab_id) + return page_404 + end + end + + def authorize_project_developer! + unless current_user.has_developer_access?(@project.gitlab_id) + return page_404 + end + end + + def authorize_manage_project! + unless current_user.can_manage_project?(@project.gitlab_id) + return page_404 + end + end + + def page_404 + render file: "#{Rails.root}/public/404.html", status: 404, layout: false + end + + # Reset user cache every day for security purposes + def reset_cache + if current_user && current_user.sync_at < (Time.zone.now - 24.hours) + current_user.reset_cache + end + end + + def default_headers + headers['X-Frame-Options'] = 'DENY' + headers['X-XSS-Protection'] = '1; mode=block' + end + + # JSON for infinite scroll via Pager object + def pager_json(partial, count) + html = render_to_string( + partial, + layout: false, + formats: [:html] + ) + + render json: { + html: html, + count: count + } + end + + def check_config + redirect_to oauth2_ci_help_path unless valid_config? + end + + def valid_config? + server = GitlabCi.config.gitlab_server + + if server.blank? || server.url.blank? || server.app_id.blank? || server.app_secret.blank? + false + else + true + end + rescue Settingslogic::MissingSetting, NoMethodError + false + end + + def invalid_token + reset_session + redirect_to ci_root_path + end + end +end diff --git a/app/controllers/ci/builds_controller.rb b/app/controllers/ci/builds_controller.rb new file mode 100644 index 00000000000..eeff3f1e0a0 --- /dev/null +++ b/app/controllers/ci/builds_controller.rb @@ -0,0 +1,77 @@ +module Ci + class BuildsController < Ci::ApplicationController + before_filter :authenticate_user!, except: [:status, :show] + before_filter :authenticate_public_page!, only: :show + before_filter :project + before_filter :authorize_access_project!, except: [:status, :show] + before_filter :authorize_manage_project!, except: [:status, :show, :retry, :cancel] + before_filter :authorize_project_developer!, only: [:retry, :cancel] + before_filter :build, except: [:show] + + def show + if params[:id] =~ /\A\d+\Z/ + @build = build + else + # try to find commit by sha + commit = commit_by_sha + + if commit + # Redirect to commit page + redirect_to ci_project_ref_commit_path(@project, @build.commit.ref, @build.commit.sha) + return + end + end + + raise ActiveRecord::RecordNotFound unless @build + + @builds = @project.commits.find_by_sha(@build.sha).builds.order('id DESC') + @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) + @commit = @build.commit + + respond_to do |format| + format.html + format.json do + render json: @build.to_json(methods: :trace_html) + end + end + end + + def retry + if @build.commands.blank? + return page_404 + end + + build = Ci::Build.retry(@build) + + if params[:return_to] + redirect_to URI.parse(params[:return_to]).path + else + redirect_to ci_project_build_path(project, build) + end + end + + def status + render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + end + + def cancel + @build.cancel + + redirect_to ci_project_build_path(@project, @build) + end + + protected + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def build + @build ||= project.builds.unscoped.find_by(id: params[:id]) + end + + def commit_by_sha + @project.commits.find_by(sha: params[:id]) + end + end +end diff --git a/app/controllers/ci/charts_controller.rb b/app/controllers/ci/charts_controller.rb new file mode 100644 index 00000000000..63326ef36cc --- /dev/null +++ b/app/controllers/ci/charts_controller.rb @@ -0,0 +1,24 @@ +module Ci + class ChartsController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_access_project! + before_filter :authorize_manage_project! + + layout 'ci/project' + + def show + @charts = {} + @charts[:week] = Ci::Charts::WeekChart.new(@project) + @charts[:month] = Ci::Charts::MonthChart.new(@project) + @charts[:year] = Ci::Charts::YearChart.new(@project) + @charts[:build_times] = Ci::Charts::BuildTime.new(@project) + end + + protected + + def project + @project = Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/commits_controller.rb b/app/controllers/ci/commits_controller.rb new file mode 100644 index 00000000000..9f74a2fd807 --- /dev/null +++ b/app/controllers/ci/commits_controller.rb @@ -0,0 +1,37 @@ +module Ci + class CommitsController < Ci::ApplicationController + before_filter :authenticate_user!, except: [:status, :show] + before_filter :authenticate_public_page!, only: :show + before_filter :project + before_filter :authorize_access_project!, except: [:status, :show, :cancel] + before_filter :authorize_project_developer!, only: [:cancel] + before_filter :commit, only: :show + + def show + @builds = @commit.builds + end + + def status + commit = Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + render json: commit.to_json(only: [:id, :sha], methods: [:status, :coverage]) + rescue ActiveRecord::RecordNotFound + render json: { status: "not_found" } + end + + def cancel + commit.builds.running_or_pending.each(&:cancel) + + redirect_to ci_project_ref_commit_path(project, commit.ref, commit.sha) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + + def commit + @commit ||= Ci::Project.find(params[:project_id]).commits.find_by_sha_and_ref!(params[:id], params[:ref_id]) + end + end +end diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb new file mode 100644 index 00000000000..c515caabe63 --- /dev/null +++ b/app/controllers/ci/events_controller.rb @@ -0,0 +1,21 @@ +module Ci + class EventsController < Ci::ApplicationController + EVENTS_PER_PAGE = 50 + + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_manage_project! + + layout 'ci/project' + + def index + @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/helps_controller.rb b/app/controllers/ci/helps_controller.rb new file mode 100644 index 00000000000..a1ee4111614 --- /dev/null +++ b/app/controllers/ci/helps_controller.rb @@ -0,0 +1,16 @@ +module Ci + class HelpsController < Ci::ApplicationController + skip_filter :check_config + + def show + end + + def oauth2 + if valid_config? + redirect_to ci_root_path + else + render layout: 'ci/empty' + end + end + end +end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb new file mode 100644 index 00000000000..62c2ba86e86 --- /dev/null +++ b/app/controllers/ci/lints_controller.rb @@ -0,0 +1,26 @@ +module Ci + class LintsController < Ci::ApplicationController + before_filter :authenticate_user! + + def show + end + + def create + if params[:content].blank? + @status = false + @error = "Please provide content of .gitlab-ci.yml" + else + @config_processor = Ci::GitlabCiYamlProcessor.new params[:content] + @stages = @config_processor.stages + @builds = @config_processor.builds + @status = true + end + rescue Ci::GitlabCiYamlProcessor::ValidationError => e + @error = e.message + @status = false + rescue Exception => e + @error = "Undefined error" + @status = false + end + end +end diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb new file mode 100644 index 00000000000..6ff7fc9f77a --- /dev/null +++ b/app/controllers/ci/projects_controller.rb @@ -0,0 +1,136 @@ +module Ci + class ProjectsController < Ci::ApplicationController + PROJECTS_BATCH = 100 + + before_filter :authenticate_user!, except: [:build, :badge, :index, :show] + before_filter :authenticate_public_page!, only: :show + before_filter :project, only: [:build, :integration, :show, :badge, :edit, :update, :destroy, :toggle_shared_runners, :dumped_yaml] + before_filter :authorize_access_project!, except: [:build, :gitlab, :badge, :index, :show, :new, :create] + before_filter :authorize_manage_project!, only: [:edit, :integration, :update, :destroy, :toggle_shared_runners, :dumped_yaml] + before_filter :authenticate_token!, only: [:build] + before_filter :no_cache, only: [:badge] + protect_from_forgery except: :build + + layout 'ci/project', except: [:index, :gitlab] + + def index + @projects = Ci::Project.ordered_by_last_commit_date.public_only.page(params[:page]) unless current_user + end + + def gitlab + @limit, @offset = (params[:limit] || PROJECTS_BATCH).to_i, (params[:offset] || 0).to_i + @page = @offset == 0 ? 1 : (@offset / @limit + 1) + + current_user.reset_cache if params[:reset_cache] + + @gl_projects = current_user.gitlab_projects(params[:search], @page, @limit) + @projects = Ci::Project.where(gitlab_id: @gl_projects.map(&:id)).ordered_by_last_commit_date + @total_count = @gl_projects.size + @gl_projects.reject! { |gl_project| @projects.map(&:gitlab_id).include?(gl_project.id) } + respond_to do |format| + format.json do + pager_json("ci/projects/gitlab", @total_count) + end + end + rescue Ci::Network::UnauthorizedError + raise + rescue + @error = 'Failed to fetch GitLab projects' + end + + def show + @ref = params[:ref] + + @commits = @project.commits.reverse_order + @commits = @commits.where(ref: @ref) if @ref + @commits = @commits.page(params[:page]).per(20) + end + + def integration + end + + def create + project_data = OpenStruct.new(JSON.parse(params["project"])) + + unless current_user.can_manage_project?(project_data.id) + return redirect_to ci_root_path, alert: 'You have to have at least master role to enable CI for this project' + end + + @project = Ci::CreateProjectService.new.execute(current_user, project_data, ci_project_url(":project_id")) + + if @project.persisted? + redirect_to ci_project_path(@project, show_guide: true), notice: 'Project was successfully created.' + else + redirect_to :back, alert: 'Cannot save project' + end + end + + def edit + end + + def update + if project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, project) + + redirect_to :back, notice: 'Project was successfully updated.' + else + render action: "edit" + end + end + + def destroy + project.destroy + Ci::Network.new.disable_ci(project.gitlab_id, current_user.authenticate_options) + + Ci::EventService.new.remove_project(current_user, project) + + redirect_to ci_projects_url + end + + def build + @commit = Ci::CreateCommitService.new.execute(@project, params.dup) + + if @commit && @commit.valid? + head 201 + else + head 400 + end + end + + # Project status badge + # Image with build status for sha or ref + def badge + image = Ci::ImageForBuildService.new.execute(@project, params) + + send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" + end + + def toggle_shared_runners + project.toggle!(:shared_runners_enabled) + redirect_to :back + end + + def dumped_yaml + send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml' + end + + protected + + def project + @project ||= Ci::Project.find(params[:id]) + end + + def no_cache + response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" + end + + def project_params + params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build, + :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients, + :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token, + { variables_attributes: [:id, :key, :value, :_destroy] }) + end + end +end diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb new file mode 100644 index 00000000000..3a52087cc6b --- /dev/null +++ b/app/controllers/ci/runner_projects_controller.rb @@ -0,0 +1,34 @@ +module Ci + class RunnerProjectsController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_manage_project! + + layout 'ci/project' + + def create + @runner = Ci::Runner.find(params[:runner_project][:runner_id]) + + return head(403) unless current_user.authorized_runners.include?(@runner) + + if @runner.assign_to(project, current_user) + redirect_to ci_project_runners_path(project) + else + redirect_to ci_project_runners_path(project), alert: 'Failed adding runner to project' + end + end + + def destroy + runner_project = project.runner_projects.find(params[:id]) + runner_project.destroy + + redirect_to ci_project_runners_path(project) + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/runners_controller.rb b/app/controllers/ci/runners_controller.rb new file mode 100644 index 00000000000..01eebf7e6a7 --- /dev/null +++ b/app/controllers/ci/runners_controller.rb @@ -0,0 +1,71 @@ +module Ci + class RunnersController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show] + before_filter :authorize_access_project! + before_filter :authorize_manage_project! + + layout 'ci/project' + + def index + @runners = @project.runners.order('id DESC') + @specific_runners = current_user.authorized_runners. + where.not(id: @runners).order('runners.id DESC').page(params[:page]).per(20) + @shared_runners = Ci::Runner.shared.active + @shared_runners_count = @shared_runners.count(:all) + end + + def edit + end + + def update + if @runner.update_attributes(runner_params) + redirect_to edit_ci_project_runner_path(@project, @runner), notice: 'Runner was successfully updated.' + else + redirect_to edit_ci_project_runner_path(@project, @runner), alert: 'Runner was not updated.' + end + end + + def destroy + if @runner.only_for?(@project) + @runner.destroy + end + + redirect_to ci_project_runners_path(@project) + end + + def resume + if @runner.update_attributes(active: true) + redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.' + else + redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.' + end + end + + def pause + if @runner.update_attributes(active: false) + redirect_to ci_project_runners_path(@project, @runner), notice: 'Runner was successfully updated.' + else + redirect_to ci_project_runners_path(@project, @runner), alert: 'Runner was not updated.' + end + end + + def show + end + + protected + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def set_runner + @runner ||= @project.runners.find(params[:id]) + end + + def runner_params + params.require(:runner).permit(:description, :tag_list, :contacted_at, :active) + end + end +end diff --git a/app/controllers/ci/services_controller.rb b/app/controllers/ci/services_controller.rb new file mode 100644 index 00000000000..e99f40f3a0a --- /dev/null +++ b/app/controllers/ci/services_controller.rb @@ -0,0 +1,59 @@ +module Ci + class ServicesController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_access_project! + before_filter :authorize_manage_project! + before_filter :service, only: [:edit, :update, :test] + + respond_to :html + + layout 'ci/project' + + def index + @project.build_missing_services + @services = @project.services.reload + end + + def edit + end + + def update + if @service.update_attributes(service_params) + redirect_to edit_ci_project_service_path(@project, @service.to_param), notice: 'Service was successfully updated.' + else + render 'edit' + end + end + + def test + last_build = @project.builds.last + + if @service.execute(last_build) + message = { notice: 'We successfully tested the service' } + else + message = { alert: 'We tried to test the service but error occurred' } + end + + redirect_to :back, message + end + + private + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def service + @service ||= @project.services.find { |service| service.to_param == params[:id] } + end + + def service_params + params.require(:service).permit( + :type, :active, :webhook, :notify_only_broken_builds, + :email_recipients, :email_only_broken_builds, :email_add_pusher, + :hipchat_token, :hipchat_room, :hipchat_server + ) + end + end +end diff --git a/app/controllers/ci/triggers_controller.rb b/app/controllers/ci/triggers_controller.rb new file mode 100644 index 00000000000..6ba37cd843e --- /dev/null +++ b/app/controllers/ci/triggers_controller.rb @@ -0,0 +1,43 @@ +module Ci + class TriggersController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_access_project! + before_filter :authorize_manage_project! + + layout 'ci/project' + + def index + @triggers = @project.triggers + @trigger = Ci::Trigger.new + end + + def create + @trigger = @project.triggers.new + @trigger.save + + if @trigger.valid? + redirect_to ci_project_triggers_path(@project) + else + @triggers = @project.triggers.select(&:persisted?) + render :index + end + end + + def destroy + trigger.destroy + + redirect_to ci_project_triggers_path(@project) + end + + private + + def trigger + @trigger ||= @project.triggers.find(params[:id]) + end + + def project + @project = Ci::Project.find(params[:project_id]) + end + end +end diff --git a/app/controllers/ci/user_sessions_controller.rb b/app/controllers/ci/user_sessions_controller.rb new file mode 100644 index 00000000000..82134c1f7ba --- /dev/null +++ b/app/controllers/ci/user_sessions_controller.rb @@ -0,0 +1,65 @@ +module Ci + class UserSessionsController < Ci::ApplicationController + before_filter :authenticate_user!, except: [:new, :callback, :auth] + + def show + @user = current_user + end + + def new + end + + def auth + unless is_oauth_state_valid?(params[:state]) + redirect_to new_ci_user_sessions_path + return + end + + redirect_to client.auth_code.authorize_url({ + redirect_uri: callback_ci_user_sessions_url, + state: params[:state] + }) + end + + def callback + unless is_oauth_state_valid?(params[:state]) + redirect_to new_ci_user_sessions_path + return + end + + token = client.auth_code.get_token(params[:code], redirect_uri: callback_ci_user_sessions_url).token + + @user_session = Ci::UserSession.new + user = @user_session.authenticate(access_token: token) + + if user && sign_in(user) + return_to = get_ouath_state_return_to(params[:state]) + redirect_to(return_to || ci_root_path) + else + @error = 'Invalid credentials' + render :new + end + + end + + def destroy + sign_out + + redirect_to new_ci_user_sessions_path + end + + protected + + def client + @client ||= ::OAuth2::Client.new( + GitlabCi.config.gitlab_server.app_id, + GitlabCi.config.gitlab_server.app_secret, + { + site: GitlabCi.config.gitlab_server.url, + authorize_url: 'oauth/authorize', + token_url: 'oauth/token' + } + ) + end + end +end diff --git a/app/controllers/ci/variables_controller.rb b/app/controllers/ci/variables_controller.rb new file mode 100644 index 00000000000..6908e0877f0 --- /dev/null +++ b/app/controllers/ci/variables_controller.rb @@ -0,0 +1,33 @@ +module Ci + class VariablesController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_access_project! + before_filter :authorize_manage_project! + + layout 'ci/project' + + def show + end + + def update + if project.update_attributes(project_params) + Ci::EventService.new.change_project_settings(current_user, project) + + redirect_to ci_project_variables_path(project), notice: 'Variables were successfully updated.' + else + render action: 'show' + end + end + + private + + def project + @project ||= Ci::Project.find(params[:project_id]) + end + + def project_params + params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + end + end +end diff --git a/app/controllers/ci/web_hooks_controller.rb b/app/controllers/ci/web_hooks_controller.rb new file mode 100644 index 00000000000..eea4842c91c --- /dev/null +++ b/app/controllers/ci/web_hooks_controller.rb @@ -0,0 +1,53 @@ +module Ci + class WebHooksController < Ci::ApplicationController + before_filter :authenticate_user! + before_filter :project + before_filter :authorize_access_project! + before_filter :authorize_manage_project! + + layout 'ci/project' + + def index + @web_hooks = @project.web_hooks + @web_hook = Ci::WebHook.new + end + + def create + @web_hook = @project.web_hooks.new(web_hook_params) + @web_hook.save + + if @web_hook.valid? + redirect_to ci_project_web_hooks_path(@project) + else + @web_hooks = @project.web_hooks.select(&:persisted?) + render :index + end + end + + def test + Ci::TestHookService.new.execute(hook, current_user) + + redirect_to :back + end + + def destroy + hook.destroy + + redirect_to ci_project_web_hooks_path(@project) + end + + private + + def hook + @web_hook ||= @project.web_hooks.find(params[:id]) + end + + def project + @project = Ci::Project.find(params[:project_id]) + end + + def web_hook_params + params.require(:web_hook).permit(:url) + end + end +end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index fc31118124b..4e007d2a4d0 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,6 +1,6 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::CurrentSettings - include PageLayoutHelper + include Gitlab::PageLayoutHelper before_action :verify_user_oauth_applications_enabled before_action :authenticate_user! diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 4193ac11399..08d94408fc8 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -1,5 +1,5 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController - include PageLayoutHelper + include Gitlab::PageLayoutHelper layout 'profile' diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index b181c47baec..b70e12365da 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -1,6 +1,6 @@ class Projects::NetworkController < Projects::ApplicationController include ExtractsPath - include ApplicationHelper + include Gitlab::ApplicationHelper before_action :require_non_empty_project before_action :assign_ref_vars diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 6080c849c8d..a9081a5ae16 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,6 +1,6 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath - include TreeHelper + include Gitlab::TreeHelper before_action :require_non_empty_project before_action :assign_ref_vars diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 50512cb6dc3..870ff035b03 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -5,7 +5,7 @@ class Projects::WikisController < Projects::ApplicationController before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_admin_wiki!, only: :destroy before_action :load_project_wiki - include WikiHelper + include Gitlab::WikiHelper def pages @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index eb0408a95e5..63d336b2bd5 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ class SearchController < ApplicationController - include SearchHelper + include Gitlab::SearchHelper layout 'search' diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb deleted file mode 100644 index 14df8d4cbd7..00000000000 --- a/app/helpers/appearances_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -module AppearancesHelper - def brand_item - nil - end - - def brand_title - 'GitLab Community Edition' - end - - def brand_image - nil - end - - def brand_text - nil - end - - def brand_header_logo - image_tag 'logo.svg' - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb deleted file mode 100644 index a803b66c502..00000000000 --- a/app/helpers/application_helper.rb +++ /dev/null @@ -1,315 +0,0 @@ -require 'digest/md5' -require 'uri' - -module ApplicationHelper - # Check if a particular controller is the current one - # - # args - One or more controller names to check - # - # Examples - # - # # On TreeController - # current_controller?(:tree) # => true - # current_controller?(:commits) # => false - # current_controller?(:commits, :tree) # => true - def current_controller?(*args) - args.any? { |v| v.to_s.downcase == controller.controller_name } - end - - # Check if a particular action is the current one - # - # args - One or more action names to check - # - # Examples - # - # # On Projects#new - # current_action?(:new) # => true - # current_action?(:create) # => false - # current_action?(:new, :create) # => true - def current_action?(*args) - args.any? { |v| v.to_s.downcase == action_name } - end - - def project_icon(project_id, options = {}) - project = - if project_id.is_a?(Project) - project = project_id - else - Project.find_with_namespace(project_id) - end - - if project.avatar_url - image_tag project.avatar_url, options - else # generated icon - project_identicon(project, options) - end - end - - def project_identicon(project, options = {}) - allowed_colors = { - red: 'FFEBEE', - purple: 'F3E5F5', - indigo: 'E8EAF6', - blue: 'E3F2FD', - teal: 'E0F2F1', - orange: 'FBE9E7', - gray: 'EEEEEE' - } - - options[:class] ||= '' - options[:class] << ' identicon' - bg_key = project.id % 7 - style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555" - - content_tag(:div, class: options[:class], style: style) do - project.name[0, 1].upcase - end - end - - def avatar_icon(user_email = '', size = nil) - user = User.find_by(email: user_email) - - if user - user.avatar_url(size) || default_avatar - else - gravatar_icon(user_email, size) - end - end - - def gravatar_icon(user_email = '', size = nil) - GravatarService.new.execute(user_email, size) || - default_avatar - end - - def default_avatar - image_path('no_avatar.png') - end - - def last_commit(project) - if project.repo_exists? - time_ago_with_tooltip(project.repository.commit.committed_date) - else - 'Never' - end - rescue - '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 - - def emoji_autocomplete_source - # should be an array of strings - # so to_s can be called, because it is sufficient and to_json is too slow - Emoji.names.to_s - end - - # Define whenever show last push event - # with suggestion to create MR - def show_last_push_widget?(event) - # Skip if event is not about added or modified non-master branch - return false unless event && event.last_push_to_non_root? && !event.rm_ref? - - project = event.project - - # Skip if project repo is empty or MR disabled - return false unless project && !project.empty_repo? && project.merge_requests_enabled - - # Skip if user already created appropriate MR - 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) - - true - end - - def hexdigest(string) - Digest::SHA1.hexdigest string - end - - def simple_sanitize(str) - sanitize(str, tags: %w(a span)) - end - - def body_data_page - path = controller.controller_path.split('/') - namespace = path.first if path.second - - [namespace, controller.controller_name, controller.action_name].compact.join(':') - end - - # shortcut for gitlab config - def gitlab_config - Gitlab.config.gitlab - end - - # shortcut for gitlab extra config - def extra_config - Gitlab.config.extra - end - - def search_placeholder - if @project && @project.persisted? - 'Search in this project' - elsif @snippet || @snippets || @show_snippets - 'Search snippets' - elsif @group && @group.persisted? - 'Search in this group' - else - 'Search' - end - end - - def broadcast_message - BroadcastMessage.current - end - - # Render a `time` element with Javascript-based relative date and tooltip - # - # time - Time object - # placement - Tooltip placement String (default: "top") - # html_class - Custom class for `time` element (default: "time_ago") - # skip_js - When true, exclude the `script` tag (default: false) - # - # By default also includes a `script` element with Javascript necessary to - # initialize the `timeago` jQuery extension. If this method is called many - # times, for example rendering hundreds of commits, it's advisable to disable - # this behavior using the `skip_js` argument and re-initializing `timeago` - # manually once all of the elements have been rendered. - # - # A `js-timeago` class is always added to the element, even when a custom - # `html_class` argument is provided. - # - # Returns an HTML-safe String - def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) - element = content_tag :time, time.to_s, - class: "#{html_class} js-timeago", - datetime: time.getutc.iso8601, - title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), - data: { toggle: 'tooltip', placement: placement } - - element += javascript_tag "$('.js-timeago').timeago()" unless skip_js - - element - end - - def render_markup(file_name, file_content) - if gitlab_markdown?(file_name) - Haml::Helpers.preserve(markdown(file_content)) - elsif asciidoc?(file_name) - asciidoc(file_content) - elsif plain?(file_name) - content_tag :pre, class: 'plain-readme' do - file_content - end - else - GitHub::Markup.render(file_name, file_content). - force_encoding(file_content.encoding).html_safe - end - rescue RuntimeError - simple_format(file_content) - end - - def plain?(filename) - Gitlab::MarkupHelper.plain?(filename) - end - - def markup?(filename) - Gitlab::MarkupHelper.markup?(filename) - end - - def gitlab_markdown?(filename) - Gitlab::MarkupHelper.gitlab_markdown?(filename) - end - - def asciidoc?(filename) - Gitlab::MarkupHelper.asciidoc?(filename) - end - - def promo_host - 'about.gitlab.com' - end - - def promo_url - 'https://' + promo_host - end - - def page_filter_path(options = {}) - without = options.delete(:without) - - exist_opts = { - state: params[:state], - scope: params[:scope], - label_name: params[:label_name], - milestone_id: params[:milestone_id], - assignee_id: params[:assignee_id], - author_id: params[:author_id], - sort: params[:sort], - } - - options = exist_opts.merge(options) - - if without.present? - without.each do |key| - options.delete(key) - end - end - - path = request.path - path << "?#{options.to_param}" - path - end - - def outdated_browser? - browser.ie? && browser.version.to_i < 10 - end - - def path_to_key(key, admin = false) - if admin - admin_user_key_path(@user, key) - else - profile_key_path(key) - end - end - - def state_filters_text_for(entity, project) - titles = { - opened: "Open" - } - - entity_title = titles[entity] || entity.to_s.humanize - - count = - if project.nil? - nil - elsif current_controller?(:issues) - project.issues.send(entity).count - elsif current_controller?(:merge_requests) - project.merge_requests.send(entity).count - end - - html = content_tag :span, entity_title - - if count.present? - html += " " - html += content_tag :span, number_with_delimiter(count), class: 'badge' - end - - html.html_safe - end -end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb deleted file mode 100644 index 7d6b58ee21a..00000000000 --- a/app/helpers/application_settings_helper.rb +++ /dev/null @@ -1,59 +0,0 @@ -module ApplicationSettingsHelper - def gravatar_enabled? - current_application_settings.gravatar_enabled? - end - - def twitter_sharing_enabled? - current_application_settings.twitter_sharing_enabled? - end - - def signup_enabled? - current_application_settings.signup_enabled? - end - - def signin_enabled? - current_application_settings.signin_enabled? - end - - def extra_sign_in_text - current_application_settings.sign_in_text - end - - def user_oauth_applications? - current_application_settings.user_oauth_applications - end - - # Return a group of checkboxes that use Bootstrap's button plugin for a - # toggle button effect. - def restricted_level_checkboxes(help_block_id) - Gitlab::VisibilityLevel.options.map do |name, level| - checked = restricted_visibility_levels(true).include?(level) - css_class = 'btn' - css_class += ' active' if checked - checkbox_name = 'application_setting[restricted_visibility_levels][]' - - label_tag(checkbox_name, class: css_class) do - check_box_tag(checkbox_name, level, checked, - autocomplete: 'off', - 'aria-describedby' => help_block_id) + name - end - end - end - - # Return a group of checkboxes that use Bootstrap's button plugin for a - # toggle button effect. - def import_sources_checkboxes(help_block_id) - Gitlab::ImportSources.options.map do |name, source| - checked = current_application_settings.import_sources.include?(source) - css_class = 'btn' - css_class += ' active' if checked - checkbox_name = 'application_setting[import_sources][]' - - label_tag(checkbox_name, class: css_class) do - check_box_tag(checkbox_name, source, checked, - autocomplete: 'off', - 'aria-describedby' => help_block_id) + name - end - end - end -end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb deleted file mode 100644 index 0e7a37b4cc6..00000000000 --- a/app/helpers/auth_helper.rb +++ /dev/null @@ -1,50 +0,0 @@ -module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze - FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze - - def ldap_enabled? - Gitlab.config.ldap.enabled - end - - def provider_has_icon?(name) - PROVIDERS_WITH_ICONS.include?(name.to_s) - end - - def auth_providers - Gitlab::OAuth::Provider.providers - end - - def label_for_provider(name) - Gitlab::OAuth::Provider.label_for(name) - end - - def form_based_provider?(name) - FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s } - end - - def form_based_providers - auth_providers.select { |provider| form_based_provider?(provider) } - end - - def button_based_providers - auth_providers.reject { |provider| form_based_provider?(provider) } - end - - def provider_image_tag(provider, size = 64) - label = label_for_provider(provider) - - if provider_has_icon?(provider) - file_name = "#{provider.to_s.split('_').first}_#{size}.png" - - image_tag(image_path("auth_buttons/#{file_name}"), alt: label, title: "Sign in with #{label}") - else - label - end - end - - def auth_active?(provider) - current_user.identities.exists?(provider: provider.to_s) - end - - extend self -end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb deleted file mode 100644 index 77d99140c43..00000000000 --- a/app/helpers/blob_helper.rb +++ /dev/null @@ -1,74 +0,0 @@ -module BlobHelper - def highlight(blob_name, blob_content, nowrap: false, continue: false) - @formatter ||= Rouge::Formatters::HTMLGitlab.new( - nowrap: nowrap, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - ) - - begin - @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new - result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe - rescue - @lexer = Rouge::Lexers::PlainText - result = @formatter.format(@lexer.lex(blob_content)).html_safe - end - - result - end - - def no_highlight_files - %w(credits changelog news copying copyright license authors) - end - - def edit_blob_link(project, ref, path, options = {}) - blob = - begin - project.repository.blob_at(ref, path) - rescue - nil - end - - if blob && blob.text? - text = 'Edit' - after = options[:after] || '' - from_mr = options[:from_merge_request_id] - link_opts = {} - link_opts[:from_merge_request_id] = from_mr if from_mr - cls = 'btn btn-small' - if allowed_tree_edit?(project, ref) - link_to(text, - namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - link_opts), - class: cls - ) - else - content_tag :span, text, class: cls + ' disabled' - end + after.html_safe - else - '' - end - end - - def leave_edit_message - "Leave edit mode?\nAll unsaved changes will be lost." - end - - def editing_preview_title(filename) - if Gitlab::MarkupHelper.previewable?(filename) - 'Preview' - else - 'Preview changes' - end - end - - # Return an image icon depending on the file mode and extension - # - # mode - File unix mode - # mode - File name - def blob_icon(mode, name) - icon("#{file_type_icon_class('file', mode, name)} fw") - end -end diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb deleted file mode 100644 index d6eaa7d57bc..00000000000 --- a/app/helpers/branches_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -module BranchesHelper - def can_remove_branch?(project, branch_name) - if project.protected_branch? branch_name - false - elsif branch_name == project.repository.root_ref - false - else - can?(current_user, :push_code, project) - end - end - - def can_push_branch?(project, branch_name) - return false unless project.repository.branch_names.include?(branch_name) - - ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) - end -end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb deleted file mode 100644 index 6484dca6b55..00000000000 --- a/app/helpers/broadcast_messages_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -module BroadcastMessagesHelper - def broadcast_styling(broadcast_message) - styling = '' - - if broadcast_message.color.present? - styling << "background-color: #{broadcast_message.color}" - styling << '; ' if broadcast_message.font.present? - end - - if broadcast_message.font.present? - styling << "color: #{broadcast_message.font}" - end - - styling - end -end diff --git a/app/helpers/ci/application_helper.rb b/app/helpers/ci/application_helper.rb new file mode 100644 index 00000000000..3198fe55f91 --- /dev/null +++ b/app/helpers/ci/application_helper.rb @@ -0,0 +1,140 @@ +module Ci + module ApplicationHelper + def loader_html + image_tag 'ci/loader.gif', alt: 'Loading' + end + + # Navigation link helper + # + # Returns an `li` element with an 'active' class if the supplied + # controller(s) and/or action(s) are currently active. The content of the + # element is the value passed to the block. + # + # options - The options hash used to determine if the element is "active" (default: {}) + # :controller - One or more controller names to check (optional). + # :action - One or more action names to check (optional). + # :path - A shorthand path, such as 'dashboard#index', to check (optional). + # :html_options - Extra options to be passed to the list element (optional). + # block - An optional block that will become the contents of the returned + # `li` element. + # + # When both :controller and :action are specified, BOTH must match in order + # to be marked as active. When only one is given, either can match. + # + # Examples + # + # # Assuming we're on TreeController#show + # + # # Controller matches, but action doesn't + # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" } + # # => '
  • Hello
  • ' + # + # # Controller matches + # nav_link(controller: [:tree, :refs]) { "Hello" } + # # => '
  • Hello
  • ' + # + # # Shorthand path + # nav_link(path: 'tree#show') { "Hello" } + # # => '
  • Hello
  • ' + # + # # Supplying custom options for the list element + # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" } + # # => '
  • Hello
  • ' + # + # Returns a list item element String + def nav_link(options = {}, &block) + if path = options.delete(:path) + if path.respond_to?(:each) + c = path.map { |p| p.split('#').first } + a = path.map { |p| p.split('#').last } + else + c, a, _ = path.split('#') + end + else + c = options.delete(:controller) + a = options.delete(:action) + end + + if c && a + # When given both options, make sure BOTH are active + klass = current_controller?(*c) && current_action?(*a) ? 'active' : '' + else + # Otherwise check EITHER option + klass = current_controller?(*c) || current_action?(*a) ? 'active' : '' + end + + # Add our custom class into the html_options, which may or may not exist + # and which may or may not already have a :class key + o = options.delete(:html_options) || {} + o[:class] ||= '' + o[:class] += ' ' + klass + o[:class].strip! + + if block_given? + content_tag(:li, capture(&block), o) + else + content_tag(:li, nil, o) + end + end + + # Check if a particular controller is the current one + # + # args - One or more controller names to check + # + # Examples + # + # # On TreeController + # current_controller?(:tree) # => true + # current_controller?(:commits) # => false + # current_controller?(:commits, :tree) # => true + def current_controller?(*args) + args.any? { |v| v.to_s.downcase == controller.controller_name } + end + + # Check if a particular action is the current one + # + # args - One or more action names to check + # + # Examples + # + # # On Projects#new + # current_action?(:new) # => true + # current_action?(:create) # => false + # current_action?(:new, :create) # => true + def current_action?(*args) + args.any? { |v| v.to_s.downcase == action_name } + end + + def date_from_to(from, to) + "#{from.to_s(:short)} - #{to.to_s(:short)}" + end + + def body_data_page + path = controller.controller_path.split('/') + namespace = path.first if path.second + + [namespace, controller.controller_name, controller.action_name].compact.join(":") + end + + def duration_in_words(finished_at, started_at) + if finished_at && started_at + interval_in_seconds = finished_at.to_i - started_at.to_i + elsif started_at + interval_in_seconds = Time.now.to_i - started_at.to_i + end + + time_interval_in_words(interval_in_seconds) + end + + def time_interval_in_words(interval_in_seconds) + minutes = interval_in_seconds / 60 + seconds = interval_in_seconds - minutes * 60 + + if minutes >= 1 + "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}" + else + "#{pluralize(seconds, "second")}" + end + end + end +end diff --git a/app/helpers/ci/builds_helper.rb b/app/helpers/ci/builds_helper.rb new file mode 100644 index 00000000000..cdabdad17d2 --- /dev/null +++ b/app/helpers/ci/builds_helper.rb @@ -0,0 +1,41 @@ +module Ci + module BuildsHelper + def build_ref_link build + gitlab_ref_link build.project, build.ref + end + + def build_compare_link build + gitlab_compare_link build.project, build.commit.short_before_sha, build.short_sha + end + + def build_commit_link build + gitlab_commit_link build.project, build.short_sha + end + + def build_url(build) + ci_project_build_url(build.project, build) + end + + def build_status_alert_class(build) + if build.success? + 'alert-success' + elsif build.failed? + 'alert-danger' + elsif build.canceled? + 'alert-disabled' + else + 'alert-warning' + end + end + + def build_icon_css_class(build) + if build.success? + 'fa-circle cgreen' + elsif build.failed? + 'fa-circle cred' + else + 'fa-circle light' + end + end + end +end diff --git a/app/helpers/ci/commits_helper.rb b/app/helpers/ci/commits_helper.rb new file mode 100644 index 00000000000..0479bc10594 --- /dev/null +++ b/app/helpers/ci/commits_helper.rb @@ -0,0 +1,26 @@ +module Ci + module CommitsHelper + def commit_status_alert_class(commit) + return unless commit + + case commit.status + when 'success' + 'alert-success' + when 'failed', 'canceled' + 'alert-danger' + when 'skipped' + 'alert-disabled' + else + 'alert-warning' + end + end + + def commit_link(commit) + link_to(commit.short_sha, ci_project_ref_commit_path(commit.project, commit.ref, commit.sha)) + end + + def truncate_first_line(message, length = 50) + truncate(message.each_line.first.chomp, length: length) if message + end + end +end diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb new file mode 100644 index 00000000000..2b89a0ce93e --- /dev/null +++ b/app/helpers/ci/gitlab_helper.rb @@ -0,0 +1,36 @@ +module Ci + module GitlabHelper + def no_turbolink + { :"data-no-turbolink" => "data-no-turbolink" } + end + + def gitlab_ref_link project, ref + gitlab_url = project.gitlab_url.dup + gitlab_url << "/commits/#{ref}" + link_to ref, gitlab_url, no_turbolink + end + + def gitlab_compare_link project, before, after + gitlab_url = project.gitlab_url.dup + gitlab_url << "/compare/#{before}...#{after}" + + link_to "#{before}...#{after}", gitlab_url, no_turbolink + end + + def gitlab_commit_link project, sha + gitlab_url = project.gitlab_url.dup + gitlab_url << "/commit/#{sha}" + link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink + end + + def yaml_web_editor_link(project) + commits = project.commits + + if commits.any? && commits.last.push_data[:ci_yaml_file] + "#{@project.gitlab_url}/edit/master/.gitlab-ci.yml" + else + "#{@project.gitlab_url}/new/master" + end + end + end +end diff --git a/app/helpers/ci/icons_helper.rb b/app/helpers/ci/icons_helper.rb new file mode 100644 index 00000000000..ecb6ef7be45 --- /dev/null +++ b/app/helpers/ci/icons_helper.rb @@ -0,0 +1,11 @@ +module Ci + module IconsHelper + def boolean_to_icon(value) + if value.to_s == "true" + content_tag :i, nil, class: 'fa-circle cgreen' + else + content_tag :i, nil, class: 'fa-power-off clgray' + end + end + end +end diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb new file mode 100644 index 00000000000..fd991a4165a --- /dev/null +++ b/app/helpers/ci/projects_helper.rb @@ -0,0 +1,36 @@ +module Ci + module ProjectsHelper + def ref_tab_class ref = nil + 'active' if ref == @ref + end + + def success_ratio(success_builds, failed_builds) + failed_builds = failed_builds.count(:all) + success_builds = success_builds.count(:all) + + return 100 if failed_builds.zero? + + ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100 + ratio.to_i + end + + def markdown_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "[![build status](#{url})](#{ci_project_url(project, ref: ref)})" + end + + def html_badge_code(project, ref) + url = status_ci_project_url(project, ref: ref, format: 'png') + "" + end + + def project_uses_specific_runner?(project) + project.runners.any? + end + + def no_runners_for_project?(project) + project.runners.blank? && + Ci::Runner.shared.blank? + end + end +end diff --git a/app/helpers/ci/routes_helper.rb b/app/helpers/ci/routes_helper.rb new file mode 100644 index 00000000000..f22d5023db5 --- /dev/null +++ b/app/helpers/ci/routes_helper.rb @@ -0,0 +1,29 @@ +module Ci + module RoutesHelper + class Base + include Gitlab::Application.routes.url_helpers + + def default_url_options + { + host: Ci::Settings.gitlab_ci['host'], + protocol: Ci::Settings.gitlab_ci['https'] ? "https" : "http", + port: Ci::Settings.gitlab_ci['port'] + } + end + end + + def url_helpers + @url_helpers ||= Ci::Base.new + end + + def self.method_missing(method, *args, &block) + @url_helpers ||= Ci::Base.new + + if @url_helpers.respond_to?(method) + @url_helpers.send(method, *args, &block) + else + super method, *args, &block + end + end + end +end diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb new file mode 100644 index 00000000000..782208ddfe4 --- /dev/null +++ b/app/helpers/ci/runners_helper.rb @@ -0,0 +1,22 @@ +module Ci + module RunnersHelper + def runner_status_icon(runner) + unless runner.contacted_at + return content_tag :i, nil, + class: "fa-warning-sign", + title: "New runner. Has not connected yet" + end + + status = + if runner.active? + runner.contacted_at > 3.hour.ago ? :online : :offline + else + :paused + end + + content_tag :i, nil, + class: "fa-circle runner-status-#{status}", + title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" + end + end +end diff --git a/app/helpers/ci/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb new file mode 100644 index 00000000000..caff54c3520 --- /dev/null +++ b/app/helpers/ci/triggers_helper.rb @@ -0,0 +1,7 @@ +module Ci + module TriggersHelper + def build_trigger_url(project_id, ref_name) + "#{Ci::Settings.gitlab_ci.url}/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger" + end + end +end diff --git a/app/helpers/ci/user_helper.rb b/app/helpers/ci/user_helper.rb new file mode 100644 index 00000000000..c332d6ed9cf --- /dev/null +++ b/app/helpers/ci/user_helper.rb @@ -0,0 +1,15 @@ +module Ci + module UserHelper + def user_avatar_url(user = nil, size = nil, default = 'identicon') + size = 40 if size.nil? || size <= 0 + + if user.blank? || user.avatar_url.blank? + 'ci/no_avatar.png' + elsif /^(http(s?):\/\/(www|secure)\.gravatar\.com\/avatar\/(\w*))/ =~ user.avatar_url + Regexp.last_match[0] + "?s=#{size}&d=#{default}" + else + user.avatar_url + end + end + end +end diff --git a/app/helpers/ci/user_sessions_helper.rb b/app/helpers/ci/user_sessions_helper.rb new file mode 100644 index 00000000000..0296a74395c --- /dev/null +++ b/app/helpers/ci/user_sessions_helper.rb @@ -0,0 +1,32 @@ +module Ci + module UserSessionsHelper + def generate_oauth_salt + SecureRandom.hex(16) + end + + def generate_oauth_hmac(salt, return_to) + return unless return_to + digest = OpenSSL::Digest.new('sha256') + key = Gitlab::Application.secrets.db_key_base + salt + OpenSSL::HMAC.hexdigest(digest, key, return_to) + end + + def generate_oauth_state(return_to) + return unless return_to + salt = generate_oauth_salt + hmac = generate_oauth_hmac(salt, return_to) + "#{salt}:#{hmac}:#{return_to}" + end + + def get_ouath_state_return_to(state) + state.split(':', 3)[2] if state + end + + def is_oauth_state_valid?(state) + return true unless state + salt, hmac, return_to = state.split(':', 3) + return false unless return_to + hmac == generate_oauth_hmac(salt, return_to) + end + end +end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb deleted file mode 100644 index d13d80be293..00000000000 --- a/app/helpers/commits_helper.rb +++ /dev/null @@ -1,183 +0,0 @@ -# encoding: utf-8 -module CommitsHelper - # Returns a link to the commit author. If the author has a matching user and - # is a member of the current @project it will link to the team member page. - # Otherwise it will link to the author email as specified in the commit. - # - # options: - # avatar: true will prepend the avatar image - # size: size of the avatar image in px - def commit_author_link(commit, options = {}) - commit_person_link(commit, options.merge(source: :author)) - end - - # Just like #author_link but for the committer. - def commit_committer_link(commit, options = {}) - commit_person_link(commit, options.merge(source: :committer)) - end - - def image_diff_class(diff) - if diff.deleted_file - "deleted" - elsif diff.new_file - "added" - else - nil - end - end - - def commit_to_html(commit, project, inline = true) - template = inline ? "inline_commit" : "commit" - escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil? - end - - # Breadcrumb links for a Project and, if applicable, a tree path - def commits_breadcrumbs - return unless @project && @ref - - # Add the root project link and the arrow icon - crumbs = content_tag(:li) do - link_to( - @project.path, - namespace_project_commits_path(@project.namespace, @project, @ref) - ) - end - - if @path - parts = @path.split('/') - - parts.each_with_index do |part, i| - crumbs << content_tag(:li) do - # The text is just the individual part, but the link needs all the parts before it - link_to( - part, - namespace_project_commits_path( - @project.namespace, - @project, - tree_join(@ref, parts[0..i].join('/')) - ) - ) - end - end - end - - crumbs.html_safe - end - - # Return Project default branch, if it present in array - # Else - first branch in array (mb last actual branch) - def commit_default_branch(project, branches) - branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop - end - - # Returns the sorted alphabetically links to branches, separated by a comma - def commit_branches_links(project, branches) - branches.sort.map do |branch| - link_to( - namespace_project_tree_path(project.namespace, project, branch) - ) do - content_tag :span, class: 'label label-gray' do - icon('code-fork') + ' ' + branch - end - end - end.join(" ").html_safe - end - - # Returns the sorted links to tags, separated by a comma - def commit_tags_links(project, tags) - sorted = VersionSorter.rsort(tags) - sorted.map do |tag| - link_to( - namespace_project_commits_path(project.namespace, project, - project.repository.find_tag(tag).name) - ) do - content_tag :span, class: 'label label-gray' do - icon('tag') + ' ' + tag - end - end - end.join(" ").html_safe - end - - def link_to_browse_code(project, commit) - if current_controller?(:projects, :commits) - if @repo.blob_at(commit.id, @path) - return link_to( - "Browse File »", - namespace_project_blob_path(project.namespace, project, - tree_join(commit.id, @path)), - class: "pull-right" - ) - elsif @path.present? - return link_to( - "Browse Dir »", - namespace_project_tree_path(project.namespace, project, - tree_join(commit.id, @path)), - class: "pull-right" - ) - end - end - link_to( - "Browse Code »", - namespace_project_tree_path(project.namespace, project, commit), - class: "pull-right" - ) - end - - protected - - # Private: Returns a link to a person. If the person has a matching user and - # is a member of the current @project it will link to the team member page. - # Otherwise it will link to the person email as specified in the commit. - # - # options: - # source: one of :author or :committer - # avatar: true will prepend the avatar image - # size: size of the avatar image in px - def commit_person_link(commit, options = {}) - user = commit.send(options[:source]) - - source_name = clean(commit.send "#{options[:source]}_name".to_sym) - 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} #{person_name}} - else - person_name - end - - options = { - class: "commit-#{options[:source]}-link has_tooltip", - data: { :'original-title' => sanitize(source_email) } - } - - if user.nil? - mail_to(source_email, text.html_safe, options) - else - link_to(text.html_safe, user_path(user), options) - end - end - - def view_file_btn(commit_sha, diff, project) - link_to( - namespace_project_blob_path(project.namespace, project, - tree_join(commit_sha, diff.new_path)), - class: 'btn btn-small view-file js-view-file' - ) do - raw('View file @') + content_tag(:span, commit_sha[0..6], - class: 'commit-short-id') - end - end - - def truncate_sha(sha) - Commit.truncate_sha(sha) - end - - def clean(string) - Sanitize.clean(string, remove_contents: true) - end -end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb deleted file mode 100644 index f1dc906cab4..00000000000 --- a/app/helpers/compare_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -module CompareHelper - def create_mr_button?(from = params[:from], to = params[:to], project = @project) - from.present? && - to.present? && - from != to && - project.merge_requests_enabled && - project.repository.branch_names.include?(from) && - project.repository.branch_names.include?(to) - end - - def create_mr_path(from = params[:from], to = params[:to], project = @project) - new_namespace_project_merge_request_path( - project.namespace, - project, - merge_request: { - source_branch: to, - target_branch: from - } - ) - end -end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb deleted file mode 100644 index c25b54eadc6..00000000000 --- a/app/helpers/dashboard_helper.rb +++ /dev/null @@ -1,9 +0,0 @@ -module DashboardHelper - def assigned_issues_dashboard_path - issues_dashboard_path(assignee_id: current_user.id) - end - - def assigned_mrs_dashboard_path - merge_requests_dashboard_path(assignee_id: current_user.id) - end -end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb deleted file mode 100644 index 1bd3ec5e0e0..00000000000 --- a/app/helpers/diff_helper.rb +++ /dev/null @@ -1,170 +0,0 @@ -module DiffHelper - def allowed_diff_size - if diff_hard_limit_enabled? - Commit::DIFF_HARD_LIMIT_FILES - else - Commit::DIFF_SAFE_FILES - end - end - - def allowed_diff_lines - if diff_hard_limit_enabled? - Commit::DIFF_HARD_LIMIT_LINES - else - Commit::DIFF_SAFE_LINES - end - end - - def safe_diff_files(diffs) - lines = 0 - safe_files = [] - diffs.first(allowed_diff_size).each do |diff| - lines += diff.diff.lines.count - break if lines > allowed_diff_lines - safe_files << Gitlab::Diff::File.new(diff) - end - safe_files - end - - def diff_hard_limit_enabled? - # Enabling hard limit allows user to see more diff information - if params[:force_show_diff].present? - true - else - false - end - end - - def generate_line_code(file_path, line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end - - def parallel_diff(diff_file, index) - lines = [] - skip_next = false - - # Building array of lines - # - # [ - # left_type, left_line_number, left_line_content, left_line_code, - # right_line_type, right_line_number, right_line_content, right_line_code - # ] - # - diff_file.diff_lines.each do |line| - - full_line = line.text - type = line.type - line_code = generate_line_code(diff_file.file_path, line) - line_new = line.new_pos - line_old = line.old_pos - - next_line = diff_file.next_line(line.index) - - if next_line - next_line_code = generate_line_code(diff_file.file_path, next_line) - next_type = next_line.type - next_line = next_line.text - end - - if type == 'match' || type.nil? - # line in the right panel is the same as in the left one - line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code] - lines.push(line) - elsif type == 'old' - if next_type == 'new' - # Left side has text removed, right side has text added - line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code] - lines.push(line) - skip_next = true - elsif next_type == 'old' || next_type.nil? - # Left side has text removed, right side doesn't have any change - # No next line code, no new line number, no new line text - line = [type, line_old, full_line, line_code, next_type, nil, " ", nil] - lines.push(line) - end - elsif type == 'new' - if skip_next - # Change has been already included in previous line so no need to do it again - skip_next = false - next - else - # Change is only on the right side, left side has no change - line = [nil, nil, " ", line_code, type, line_new, full_line, line_code] - lines.push(line) - end - end - end - lines - end - - def unfold_bottom_class(bottom) - (bottom) ? 'js-unfold-bottom' : '' - end - - def unfold_class(unfold) - (unfold) ? 'unfold js-unfold' : '' - end - - def diff_line_content(line) - if line.blank? - "  " - else - line - end - end - - def line_comments - @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code) - end - - def organize_comments(type_left, type_right, line_code_left, line_code_right) - comments_left = comments_right = nil - - unless type_left.nil? && type_right == 'new' - comments_left = line_comments[line_code_left] - end - - unless type_left.nil? && type_right.nil? - comments_right = line_comments[line_code_right] - end - - [comments_left, comments_right] - end - - def inline_diff_btn - params_copy = params.dup - params_copy[:view] = 'inline' - # Always use HTML to handle case where JSON diff rendered this button - params_copy.delete(:format) - - link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do - 'Inline' - end - end - - def parallel_diff_btn - params_copy = params.dup - params_copy[:view] = 'parallel' - # Always use HTML to handle case where JSON diff rendered this button - params_copy.delete(:format) - - link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do - 'Side-by-side' - end - end - - def submodule_link(blob, ref, repository = @repository) - tree, commit = submodule_links(blob, ref, repository) - commit_id = if commit.nil? - blob.id[0..10] - else - link_to "#{blob.id[0..10]}", commit - end - - [ - content_tag(:span, link_to(truncate(blob.name, length: 40), tree)), - '@', - content_tag(:span, commit_id, class: 'monospace'), - ].join(' ').html_safe - end -end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb deleted file mode 100644 index 45788ba95ac..00000000000 --- a/app/helpers/emails_helper.rb +++ /dev/null @@ -1,57 +0,0 @@ -module EmailsHelper - - # Google Actions - # https://developers.google.com/gmail/markup/reference/go-to-action - def email_action(url) - name = action_title(url) - if name - data = { - "@context" => "http://schema.org", - "@type" => "EmailMessage", - "action" => { - "@type" => "ViewAction", - "name" => name, - "url" => url, - } - } - - content_tag :script, type: 'application/ld+json' do - data.to_json.html_safe - end - end - end - - def action_title(url) - return unless url - ["merge_requests", "issues", "commit"].each do |action| - if url.split("/").include?(action) - return "View #{action.humanize.singularize}" - end - end - end - - def color_email_diff(diffcontent) - formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') - lexer = Rouge::Lexers::Diff - raw formatter.format(lexer.lex(diffcontent)) - end - - def password_reset_token_valid_time - valid_hours = Devise.reset_password_within / 60 / 60 - if valid_hours >= 24 - unit = 'day' - valid_length = (valid_hours / 24).floor - else - unit = 'hour' - valid_length = valid_hours.floor - end - - pluralize(valid_length, unit) - end - - def reset_token_expire_message - link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email)) - msg = "This link is valid for #{password_reset_token_valid_time}. " - msg << "After it expires, you can #{link_tag}." - end -end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb deleted file mode 100644 index 8428281f8f6..00000000000 --- a/app/helpers/events_helper.rb +++ /dev/null @@ -1,203 +0,0 @@ -module EventsHelper - def link_to_author(event) - author = event.author - - if author - link_to author.name, user_path(author.username) - else - event.author_name - end - end - - def event_action_name(event) - target = if event.target_type - if event.note? - event.note_target_type - else - event.target_type.titleize.downcase - end - else - 'project' - end - - [event.action_name, target].join(" ") - end - - def event_filter_link(key, tooltip) - key = key.to_s - active = 'active' if @event_filter.active?(key) - link_opts = { - class: 'event_filter_link', - id: "#{key}_event_filter", - title: "Filter by #{tooltip.downcase}", - data: { toggle: 'tooltip', placement: 'top' } - } - - content_tag :li, class: "filter_icon #{active}" do - link_to request.path, link_opts do - icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip) - end - end - end - - def icon_for_event - { - EventFilter.push => 'upload', - EventFilter.merged => 'check-square-o', - EventFilter.comments => 'comments', - EventFilter.team => 'user', - } - end - - def event_feed_title(event) - words = [] - words << event.author_name - words << event_action_name(event) - - if event.push? - words << event.ref_type - words << event.ref_name - words << "at" - elsif event.commented? - if event.note_commit? - words << event.note_short_commit_id - else - words << "##{truncate event.note_target_iid}" - end - words << "at" - elsif event.target - words << "##{event.target_iid}:" - words << event.target.title if event.target.respond_to?(:title) - words << "at" - end - - words << event.project_name - - words.join(" ") - end - - def event_feed_url(event) - if event.issue? - namespace_project_issue_url(event.project.namespace, event.project, - event.issue) - elsif event.merge_request? - namespace_project_merge_request_url(event.project.namespace, - event.project, event.merge_request) - elsif event.note? && event.note_commit? - namespace_project_commit_url(event.project.namespace, event.project, - event.note_target) - elsif event.note? - if event.note_target - if event.note_commit? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)) - elsif event.note_project_snippet? - namespace_project_snippet_path(event.project.namespace, - event.project, event.note_target) - else - event_note_target_path(event) - end - end - elsif event.push? - if event.push_with_commits? && event.md_ref? - if event.commits_count > 1 - namespace_project_compare_url(event.project.namespace, event.project, - from: event.commit_from, to: - event.commit_to) - else - namespace_project_commit_url(event.project.namespace, event.project, - id: event.commit_to) - end - else - namespace_project_commits_url(event.project.namespace, event.project, - event.ref_name) - end - end - end - - def event_feed_summary(event) - if event.issue? - render "events/event_issue", issue: event.issue - elsif event.push? - render "events/event_push", event: event - elsif event.merge_request? - render "events/event_merge_request", merge_request: event.merge_request - elsif event.note? - render "events/event_note", note: event.note - end - end - - def event_note_target_path(event) - if event.note? && event.note_commit? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_target) - else - polymorphic_path([event.project.namespace.becomes(Namespace), - event.project, event.note_target], - anchor: dom_id(event.target)) - end - end - - def event_note_title_html(event) - if event.note_target - if event.note_commit? - link_to( - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)), - class: "commit_short_id" - ) do - "#{event.note_target_type} #{event.note_short_commit_id}" - end - elsif event.note_project_snippet? - link_to(namespace_project_snippet_path(event.project.namespace, - event.project, - event.note_target)) do - "#{event.note_target_type} ##{truncate event.note_target_id}" - end - else - link_to event_note_target_path(event) do - "#{event.note_target_type} ##{truncate event.note_target_iid}" - end - end - else - content_tag :strong do - "(deleted)" - end - end - end - - def event_note(text, options = {}) - text = first_line_in_markdown(text, 150, options) - sanitize(text, tags: %w(a img b pre code p span)) - end - - def event_commit_title(message) - escape_once(truncate(message.split("\n").first, length: 70)) - rescue - "--broken encoding" - end - - def event_to_atom(xml, event) - if event.proper? - xml.entry do - event_link = event_feed_url(event) - event_title = event_feed_title(event) - event_summary = event_feed_summary(event) - - xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" - xml.link href: event_link - xml.title truncate(event_title, length: 80) - xml.updated event.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email) - xml.author do |author| - xml.name event.author_name - xml.email event.author_email - end - - xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? } - end - end - end -end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb deleted file mode 100644 index 0d291f9a87e..00000000000 --- a/app/helpers/explore_helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ExploreHelper - def explore_projects_filter_path(options={}) - exist_opts = { - sort: params[:sort], - scope: params[:scope], - group: params[:group], - tag: params[:tag], - visibility_level: params[:visibility_level], - } - - options = exist_opts.merge(options) - - path = explore_projects_path - path << "?#{options.to_param}" - path - end -end diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb deleted file mode 100644 index 838b85afdfe..00000000000 --- a/app/helpers/external_wiki_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ExternalWikiHelper - def get_project_wiki_path(project) - external_wiki_service = project.services. - select { |service| service.to_param == 'external_wiki' }.first - if external_wiki_service.present? && external_wiki_service.active? - external_wiki_service.properties['external_wiki_url'] - else - namespace_project_wiki_path(project.namespace, project, :home) - end - end -end diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb deleted file mode 100644 index 09684955233..00000000000 --- a/app/helpers/git_helper.rb +++ /dev/null @@ -1,5 +0,0 @@ -module GitHelper - def strip_gpg_signature(text) - text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "") - end -end diff --git a/app/helpers/gitlab/appearances_helper.rb b/app/helpers/gitlab/appearances_helper.rb new file mode 100644 index 00000000000..54cafcd9e40 --- /dev/null +++ b/app/helpers/gitlab/appearances_helper.rb @@ -0,0 +1,23 @@ +module Gitlab + module AppearancesHelper + def brand_item + nil + end + + def brand_title + 'GitLab Community Edition' + end + + def brand_image + nil + end + + def brand_text + nil + end + + def brand_header_logo + image_tag 'logo.svg' + end + end +end diff --git a/app/helpers/gitlab/application_helper.rb b/app/helpers/gitlab/application_helper.rb new file mode 100644 index 00000000000..b019ffa5fe2 --- /dev/null +++ b/app/helpers/gitlab/application_helper.rb @@ -0,0 +1,317 @@ +require 'digest/md5' +require 'uri' + +module Gitlab + module ApplicationHelper + # Check if a particular controller is the current one + # + # args - One or more controller names to check + # + # Examples + # + # # On TreeController + # current_controller?(:tree) # => true + # current_controller?(:commits) # => false + # current_controller?(:commits, :tree) # => true + def current_controller?(*args) + args.any? { |v| v.to_s.downcase == controller.controller_name } + end + + # Check if a particular action is the current one + # + # args - One or more action names to check + # + # Examples + # + # # On Projects#new + # current_action?(:new) # => true + # current_action?(:create) # => false + # current_action?(:new, :create) # => true + def current_action?(*args) + args.any? { |v| v.to_s.downcase == action_name } + end + + def project_icon(project_id, options = {}) + project = + if project_id.is_a?(Project) + project = project_id + else + Project.find_with_namespace(project_id) + end + + if project.avatar_url + image_tag project.avatar_url, options + else # generated icon + project_identicon(project, options) + end + end + + def project_identicon(project, options = {}) + allowed_colors = { + red: 'FFEBEE', + purple: 'F3E5F5', + indigo: 'E8EAF6', + blue: 'E3F2FD', + teal: 'E0F2F1', + orange: 'FBE9E7', + gray: 'EEEEEE' + } + + options[:class] ||= '' + options[:class] << ' identicon' + bg_key = project.id % 7 + style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555" + + content_tag(:div, class: options[:class], style: style) do + project.name[0, 1].upcase + end + end + + def avatar_icon(user_email = '', size = nil) + user = User.find_by(email: user_email) + + if user + user.avatar_url(size) || default_avatar + else + gravatar_icon(user_email, size) + end + end + + def gravatar_icon(user_email = '', size = nil) + GravatarService.new.execute(user_email, size) || + default_avatar + end + + def default_avatar + image_path('no_avatar.png') + end + + def last_commit(project) + if project.repo_exists? + time_ago_with_tooltip(project.repository.commit.committed_date) + else + 'Never' + end + rescue + '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 + + def emoji_autocomplete_source + # should be an array of strings + # so to_s can be called, because it is sufficient and to_json is too slow + Emoji.names.to_s + end + + # Define whenever show last push event + # with suggestion to create MR + def show_last_push_widget?(event) + # Skip if event is not about added or modified non-master branch + return false unless event && event.last_push_to_non_root? && !event.rm_ref? + + project = event.project + + # Skip if project repo is empty or MR disabled + return false unless project && !project.empty_repo? && project.merge_requests_enabled + + # Skip if user already created appropriate MR + 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) + + true + end + + def hexdigest(string) + Digest::SHA1.hexdigest string + end + + def simple_sanitize(str) + sanitize(str, tags: %w(a span)) + end + + def body_data_page + path = controller.controller_path.split('/') + namespace = path.first if path.second + + [namespace, controller.controller_name, controller.action_name].compact.join(':') + end + + # shortcut for gitlab config + def gitlab_config + Gitlab.config.gitlab + end + + # shortcut for gitlab extra config + def extra_config + Gitlab.config.extra + end + + def search_placeholder + if @project && @project.persisted? + 'Search in this project' + elsif @snippet || @snippets || @show_snippets + 'Search snippets' + elsif @group && @group.persisted? + 'Search in this group' + else + 'Search' + end + end + + def broadcast_message + BroadcastMessage.current + end + + # Render a `time` element with Javascript-based relative date and tooltip + # + # time - Time object + # placement - Tooltip placement String (default: "top") + # html_class - Custom class for `time` element (default: "time_ago") + # skip_js - When true, exclude the `script` tag (default: false) + # + # By default also includes a `script` element with Javascript necessary to + # initialize the `timeago` jQuery extension. If this method is called many + # times, for example rendering hundreds of commits, it's advisable to disable + # this behavior using the `skip_js` argument and re-initializing `timeago` + # manually once all of the elements have been rendered. + # + # A `js-timeago` class is always added to the element, even when a custom + # `html_class` argument is provided. + # + # Returns an HTML-safe String + def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) + element = content_tag :time, time.to_s, + class: "#{html_class} js-timeago", + datetime: time.getutc.iso8601, + title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), + data: { toggle: 'tooltip', placement: placement } + + element += javascript_tag "$('.js-timeago').timeago()" unless skip_js + + element + end + + def render_markup(file_name, file_content) + if gitlab_markdown?(file_name) + Haml::Helpers.preserve(markdown(file_content)) + elsif asciidoc?(file_name) + asciidoc(file_content) + elsif plain?(file_name) + content_tag :pre, class: 'plain-readme' do + file_content + end + else + GitHub::Markup.render(file_name, file_content). + force_encoding(file_content.encoding).html_safe + end + rescue RuntimeError + simple_format(file_content) + end + + def plain?(filename) + Gitlab::MarkupHelper.plain?(filename) + end + + def markup?(filename) + Gitlab::MarkupHelper.markup?(filename) + end + + def gitlab_markdown?(filename) + Gitlab::MarkupHelper.gitlab_markdown?(filename) + end + + def asciidoc?(filename) + Gitlab::MarkupHelper.asciidoc?(filename) + end + + def promo_host + 'about.gitlab.com' + end + + def promo_url + 'https://' + promo_host + end + + def page_filter_path(options = {}) + without = options.delete(:without) + + exist_opts = { + state: params[:state], + scope: params[:scope], + label_name: params[:label_name], + milestone_id: params[:milestone_id], + assignee_id: params[:assignee_id], + author_id: params[:author_id], + sort: params[:sort], + } + + options = exist_opts.merge(options) + + if without.present? + without.each do |key| + options.delete(key) + end + end + + path = request.path + path << "?#{options.to_param}" + path + end + + def outdated_browser? + browser.ie? && browser.version.to_i < 10 + end + + def path_to_key(key, admin = false) + if admin + admin_user_key_path(@user, key) + else + profile_key_path(key) + end + end + + def state_filters_text_for(entity, project) + titles = { + opened: "Open" + } + + entity_title = titles[entity] || entity.to_s.humanize + + count = + if project.nil? + nil + elsif current_controller?(:issues) + project.issues.send(entity).count + elsif current_controller?(:merge_requests) + project.merge_requests.send(entity).count + end + + html = content_tag :span, entity_title + + if count.present? + html += " " + html += content_tag :span, number_with_delimiter(count), class: 'badge' + end + + html.html_safe + end + end +end diff --git a/app/helpers/gitlab/application_settings_helper.rb b/app/helpers/gitlab/application_settings_helper.rb new file mode 100644 index 00000000000..7132d3dcdad --- /dev/null +++ b/app/helpers/gitlab/application_settings_helper.rb @@ -0,0 +1,61 @@ +module Gitlab + module ApplicationSettingsHelper + def gravatar_enabled? + current_application_settings.gravatar_enabled? + end + + def twitter_sharing_enabled? + current_application_settings.twitter_sharing_enabled? + end + + def signup_enabled? + current_application_settings.signup_enabled? + end + + def signin_enabled? + current_application_settings.signin_enabled? + end + + def extra_sign_in_text + current_application_settings.sign_in_text + end + + def user_oauth_applications? + current_application_settings.user_oauth_applications + end + + # Return a group of checkboxes that use Bootstrap's button plugin for a + # toggle button effect. + def restricted_level_checkboxes(help_block_id) + Gitlab::VisibilityLevel.options.map do |name, level| + checked = restricted_visibility_levels(true).include?(level) + css_class = 'btn' + css_class += ' active' if checked + checkbox_name = 'application_setting[restricted_visibility_levels][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, level, checked, + autocomplete: 'off', + 'aria-describedby' => help_block_id) + name + end + end + end + + # Return a group of checkboxes that use Bootstrap's button plugin for a + # toggle button effect. + def import_sources_checkboxes(help_block_id) + Gitlab::ImportSources.options.map do |name, source| + checked = current_application_settings.import_sources.include?(source) + css_class = 'btn' + css_class += ' active' if checked + checkbox_name = 'application_setting[import_sources][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, source, checked, + autocomplete: 'off', + 'aria-describedby' => help_block_id) + name + end + end + end + end +end diff --git a/app/helpers/gitlab/auth_helper.rb b/app/helpers/gitlab/auth_helper.rb new file mode 100644 index 00000000000..fbd52dbca3d --- /dev/null +++ b/app/helpers/gitlab/auth_helper.rb @@ -0,0 +1,52 @@ +module Gitlab + module AuthHelper + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze + FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze + + def ldap_enabled? + Gitlab.config.ldap.enabled + end + + def provider_has_icon?(name) + PROVIDERS_WITH_ICONS.include?(name.to_s) + end + + def auth_providers + Gitlab::OAuth::Provider.providers + end + + def label_for_provider(name) + Gitlab::OAuth::Provider.label_for(name) + end + + def form_based_provider?(name) + FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s } + end + + def form_based_providers + auth_providers.select { |provider| form_based_provider?(provider) } + end + + def button_based_providers + auth_providers.reject { |provider| form_based_provider?(provider) } + end + + def provider_image_tag(provider, size = 64) + label = label_for_provider(provider) + + if provider_has_icon?(provider) + file_name = "#{provider.to_s.split('_').first}_#{size}.png" + + image_tag("auth_buttons/#{file_name}", alt: label, title: "Sign in with #{label}") + else + label + end + end + + def auth_active?(provider) + current_user.identities.exists?(provider: provider.to_s) + end + + extend self + end +end diff --git a/app/helpers/gitlab/blob_helper.rb b/app/helpers/gitlab/blob_helper.rb new file mode 100644 index 00000000000..8b53ba8b54f --- /dev/null +++ b/app/helpers/gitlab/blob_helper.rb @@ -0,0 +1,76 @@ +module Gitlab + module BlobHelper + def highlight(blob_name, blob_content, nowrap: false, continue: false) + @formatter ||= Rouge::Formatters::HTMLGitlab.new( + nowrap: nowrap, + cssclass: 'code highlight', + lineanchors: true, + lineanchorsid: 'LC' + ) + + begin + @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new + result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe + rescue + @lexer = Rouge::Lexers::PlainText + result = @formatter.format(@lexer.lex(blob_content)).html_safe + end + + result + end + + def no_highlight_files + %w(credits changelog news copying copyright license authors) + end + + def edit_blob_link(project, ref, path, options = {}) + blob = + begin + project.repository.blob_at(ref, path) + rescue + nil + end + + if blob && blob.text? + text = 'Edit' + after = options[:after] || '' + from_mr = options[:from_merge_request_id] + link_opts = {} + link_opts[:from_merge_request_id] = from_mr if from_mr + cls = 'btn btn-small' + if allowed_tree_edit?(project, ref) + link_to(text, + namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + link_opts), + class: cls + ) + else + content_tag :span, text, class: cls + ' disabled' + end + after.html_safe + else + '' + end + end + + def leave_edit_message + "Leave edit mode?\nAll unsaved changes will be lost." + end + + def editing_preview_title(filename) + if Gitlab::MarkupHelper.previewable?(filename) + 'Preview' + else + 'Preview changes' + end + end + + # Return an image icon depending on the file mode and extension + # + # mode - File unix mode + # mode - File name + def blob_icon(mode, name) + icon("#{file_type_icon_class('file', mode, name)} fw") + end + end +end diff --git a/app/helpers/gitlab/branches_helper.rb b/app/helpers/gitlab/branches_helper.rb new file mode 100644 index 00000000000..ecc56002e84 --- /dev/null +++ b/app/helpers/gitlab/branches_helper.rb @@ -0,0 +1,19 @@ +module Gitlab + module BranchesHelper + def can_remove_branch?(project, branch_name) + if project.protected_branch? branch_name + false + elsif branch_name == project.repository.root_ref + false + else + can?(current_user, :push_code, project) + end + end + + def can_push_branch?(project, branch_name) + return false unless project.repository.branch_names.include?(branch_name) + + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) + end + end +end diff --git a/app/helpers/gitlab/broadcast_messages_helper.rb b/app/helpers/gitlab/broadcast_messages_helper.rb new file mode 100644 index 00000000000..93f0b0ec5ae --- /dev/null +++ b/app/helpers/gitlab/broadcast_messages_helper.rb @@ -0,0 +1,18 @@ +module Gitlab + module BroadcastMessagesHelper + def broadcast_styling(broadcast_message) + styling = '' + + if broadcast_message.color.present? + styling << "background-color: #{broadcast_message.color}" + styling << '; ' if broadcast_message.font.present? + end + + if broadcast_message.font.present? + styling << "color: #{broadcast_message.font}" + end + + styling + end + end +end diff --git a/app/helpers/gitlab/commits_helper.rb b/app/helpers/gitlab/commits_helper.rb new file mode 100644 index 00000000000..8a3de838b39 --- /dev/null +++ b/app/helpers/gitlab/commits_helper.rb @@ -0,0 +1,185 @@ +# encoding: utf-8 +module Gitlab + module CommitsHelper + # Returns a link to the commit author. If the author has a matching user and + # is a member of the current @project it will link to the team member page. + # Otherwise it will link to the author email as specified in the commit. + # + # options: + # avatar: true will prepend the avatar image + # size: size of the avatar image in px + def commit_author_link(commit, options = {}) + commit_person_link(commit, options.merge(source: :author)) + end + + # Just like #author_link but for the committer. + def commit_committer_link(commit, options = {}) + commit_person_link(commit, options.merge(source: :committer)) + end + + def image_diff_class(diff) + if diff.deleted_file + "deleted" + elsif diff.new_file + "added" + else + nil + end + end + + def commit_to_html(commit, project, inline = true) + template = inline ? "inline_commit" : "commit" + escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil? + end + + # Breadcrumb links for a Project and, if applicable, a tree path + def commits_breadcrumbs + return unless @project && @ref + + # Add the root project link and the arrow icon + crumbs = content_tag(:li) do + link_to( + @project.path, + namespace_project_commits_path(@project.namespace, @project, @ref) + ) + end + + if @path + parts = @path.split('/') + + parts.each_with_index do |part, i| + crumbs << content_tag(:li) do + # The text is just the individual part, but the link needs all the parts before it + link_to( + part, + namespace_project_commits_path( + @project.namespace, + @project, + tree_join(@ref, parts[0..i].join('/')) + ) + ) + end + end + end + + crumbs.html_safe + end + + # Return Project default branch, if it present in array + # Else - first branch in array (mb last actual branch) + def commit_default_branch(project, branches) + branches.include?(project.default_branch) ? branches.delete(project.default_branch) : branches.pop + end + + # Returns the sorted alphabetically links to branches, separated by a comma + def commit_branches_links(project, branches) + branches.sort.map do |branch| + link_to( + namespace_project_tree_path(project.namespace, project, branch) + ) do + content_tag :span, class: 'label label-gray' do + icon('code-fork') + ' ' + branch + end + end + end.join(" ").html_safe + end + + # Returns the sorted links to tags, separated by a comma + def commit_tags_links(project, tags) + sorted = VersionSorter.rsort(tags) + sorted.map do |tag| + link_to( + namespace_project_commits_path(project.namespace, project, + project.repository.find_tag(tag).name) + ) do + content_tag :span, class: 'label label-gray' do + icon('tag') + ' ' + tag + end + end + end.join(" ").html_safe + end + + def link_to_browse_code(project, commit) + if current_controller?(:projects, :commits) + if @repo.blob_at(commit.id, @path) + return link_to( + "Browse File »", + namespace_project_blob_path(project.namespace, project, + tree_join(commit.id, @path)), + class: "pull-right" + ) + elsif @path.present? + return link_to( + "Browse Dir »", + namespace_project_tree_path(project.namespace, project, + tree_join(commit.id, @path)), + class: "pull-right" + ) + end + end + link_to( + "Browse Code »", + namespace_project_tree_path(project.namespace, project, commit), + class: "pull-right" + ) + end + + protected + + # Private: Returns a link to a person. If the person has a matching user and + # is a member of the current @project it will link to the team member page. + # Otherwise it will link to the person email as specified in the commit. + # + # options: + # source: one of :author or :committer + # avatar: true will prepend the avatar image + # size: size of the avatar image in px + def commit_person_link(commit, options = {}) + user = commit.send(options[:source]) + + source_name = clean(commit.send "#{options[:source]}_name".to_sym) + 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} #{person_name}} + else + person_name + end + + options = { + class: "commit-#{options[:source]}-link has_tooltip", + data: { :'original-title' => sanitize(source_email) } + } + + if user.nil? + mail_to(source_email, text.html_safe, options) + else + link_to(text.html_safe, user_path(user), options) + end + end + + def view_file_btn(commit_sha, diff, project) + link_to( + namespace_project_blob_path(project.namespace, project, + tree_join(commit_sha, diff.new_path)), + class: 'btn btn-small view-file js-view-file' + ) do + raw('View file @') + content_tag(:span, commit_sha[0..6], + class: 'commit-short-id') + end + end + + def truncate_sha(sha) + Commit.truncate_sha(sha) + end + + def clean(string) + Sanitize.clean(string, remove_contents: true) + end + end +end diff --git a/app/helpers/gitlab/compare_helper.rb b/app/helpers/gitlab/compare_helper.rb new file mode 100644 index 00000000000..407d25d3102 --- /dev/null +++ b/app/helpers/gitlab/compare_helper.rb @@ -0,0 +1,23 @@ +module Gitlab + module CompareHelper + def create_mr_button?(from = params[:from], to = params[:to], project = @project) + from.present? && + to.present? && + from != to && + project.merge_requests_enabled && + project.repository.branch_names.include?(from) && + project.repository.branch_names.include?(to) + end + + def create_mr_path(from = params[:from], to = params[:to], project = @project) + new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_branch: to, + target_branch: from + } + ) + end + end +end diff --git a/app/helpers/gitlab/dashboard_helper.rb b/app/helpers/gitlab/dashboard_helper.rb new file mode 100644 index 00000000000..2211c93999e --- /dev/null +++ b/app/helpers/gitlab/dashboard_helper.rb @@ -0,0 +1,11 @@ +module Gitlab + module DashboardHelper + def assigned_issues_dashboard_path + issues_dashboard_path(assignee_id: current_user.id) + end + + def assigned_mrs_dashboard_path + merge_requests_dashboard_path(assignee_id: current_user.id) + end + end +end diff --git a/app/helpers/gitlab/diff_helper.rb b/app/helpers/gitlab/diff_helper.rb new file mode 100644 index 00000000000..02907eb80f3 --- /dev/null +++ b/app/helpers/gitlab/diff_helper.rb @@ -0,0 +1,172 @@ +module Gitlab + module DiffHelper + def allowed_diff_size + if diff_hard_limit_enabled? + Commit::DIFF_HARD_LIMIT_FILES + else + Commit::DIFF_SAFE_FILES + end + end + + def allowed_diff_lines + if diff_hard_limit_enabled? + Commit::DIFF_HARD_LIMIT_LINES + else + Commit::DIFF_SAFE_LINES + end + end + + def safe_diff_files(diffs) + lines = 0 + safe_files = [] + diffs.first(allowed_diff_size).each do |diff| + lines += diff.diff.lines.count + break if lines > allowed_diff_lines + safe_files << Gitlab::Diff::File.new(diff) + end + safe_files + end + + def diff_hard_limit_enabled? + # Enabling hard limit allows user to see more diff information + if params[:force_show_diff].present? + true + else + false + end + end + + def generate_line_code(file_path, line) + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + end + + def parallel_diff(diff_file, index) + lines = [] + skip_next = false + + # Building array of lines + # + # [ + # left_type, left_line_number, left_line_content, left_line_code, + # right_line_type, right_line_number, right_line_content, right_line_code + # ] + # + diff_file.diff_lines.each do |line| + + full_line = line.text + type = line.type + line_code = generate_line_code(diff_file.file_path, line) + line_new = line.new_pos + line_old = line.old_pos + + next_line = diff_file.next_line(line.index) + + if next_line + next_line_code = generate_line_code(diff_file.file_path, next_line) + next_type = next_line.type + next_line = next_line.text + end + + if type == 'match' || type.nil? + # line in the right panel is the same as in the left one + line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code] + lines.push(line) + elsif type == 'old' + if next_type == 'new' + # Left side has text removed, right side has text added + line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code] + lines.push(line) + skip_next = true + elsif next_type == 'old' || next_type.nil? + # Left side has text removed, right side doesn't have any change + # No next line code, no new line number, no new line text + line = [type, line_old, full_line, line_code, next_type, nil, " ", nil] + lines.push(line) + end + elsif type == 'new' + if skip_next + # Change has been already included in previous line so no need to do it again + skip_next = false + next + else + # Change is only on the right side, left side has no change + line = [nil, nil, " ", line_code, type, line_new, full_line, line_code] + lines.push(line) + end + end + end + lines + end + + def unfold_bottom_class(bottom) + (bottom) ? 'js-unfold-bottom' : '' + end + + def unfold_class(unfold) + (unfold) ? 'unfold js-unfold' : '' + end + + def diff_line_content(line) + if line.blank? + "  " + else + line + end + end + + def line_comments + @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code) + end + + def organize_comments(type_left, type_right, line_code_left, line_code_right) + comments_left = comments_right = nil + + unless type_left.nil? && type_right == 'new' + comments_left = line_comments[line_code_left] + end + + unless type_left.nil? && type_right.nil? + comments_right = line_comments[line_code_right] + end + + [comments_left, comments_right] + end + + def inline_diff_btn + params_copy = params.dup + params_copy[:view] = 'inline' + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) + + link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do + 'Inline' + end + end + + def parallel_diff_btn + params_copy = params.dup + params_copy[:view] = 'parallel' + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) + + link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do + 'Side-by-side' + end + end + + def submodule_link(blob, ref, repository = @repository) + tree, commit = submodule_links(blob, ref, repository) + commit_id = if commit.nil? + blob.id[0..10] + else + link_to "#{blob.id[0..10]}", commit + end + + [ + content_tag(:span, link_to(truncate(blob.name, length: 40), tree)), + '@', + content_tag(:span, commit_id, class: 'monospace'), + ].join(' ').html_safe + end + end +end diff --git a/app/helpers/gitlab/emails_helper.rb b/app/helpers/gitlab/emails_helper.rb new file mode 100644 index 00000000000..84f106dd536 --- /dev/null +++ b/app/helpers/gitlab/emails_helper.rb @@ -0,0 +1,59 @@ +module Gitlab + module EmailsHelper + + # Google Actions + # https://developers.google.com/gmail/markup/reference/go-to-action + def email_action(url) + name = action_title(url) + if name + data = { + "@context" => "http://schema.org", + "@type" => "EmailMessage", + "action" => { + "@type" => "ViewAction", + "name" => name, + "url" => url, + } + } + + content_tag :script, type: 'application/ld+json' do + data.to_json.html_safe + end + end + end + + def action_title(url) + return unless url + ["merge_requests", "issues", "commit"].each do |action| + if url.split("/").include?(action) + return "View #{action.humanize.singularize}" + end + end + end + + def color_email_diff(diffcontent) + formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') + lexer = Rouge::Lexers::Diff + raw formatter.format(lexer.lex(diffcontent)) + end + + def password_reset_token_valid_time + valid_hours = Devise.reset_password_within / 60 / 60 + if valid_hours >= 24 + unit = 'day' + valid_length = (valid_hours / 24).floor + else + unit = 'hour' + valid_length = valid_hours.floor + end + + pluralize(valid_length, unit) + end + + def reset_token_expire_message + link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email)) + msg = "This link is valid for #{password_reset_token_valid_time}. " + msg << "After it expires, you can #{link_tag}." + end + end +end diff --git a/app/helpers/gitlab/events_helper.rb b/app/helpers/gitlab/events_helper.rb new file mode 100644 index 00000000000..65522dae533 --- /dev/null +++ b/app/helpers/gitlab/events_helper.rb @@ -0,0 +1,205 @@ +module Gitlab + module EventsHelper + def link_to_author(event) + author = event.author + + if author + link_to author.name, user_path(author.username) + else + event.author_name + end + end + + def event_action_name(event) + target = if event.target_type + if event.note? + event.note_target_type + else + event.target_type.titleize.downcase + end + else + 'project' + end + + [event.action_name, target].join(" ") + end + + def event_filter_link(key, tooltip) + key = key.to_s + active = 'active' if @event_filter.active?(key) + link_opts = { + class: 'event_filter_link', + id: "#{key}_event_filter", + title: "Filter by #{tooltip.downcase}", + data: { toggle: 'tooltip', placement: 'top' } + } + + content_tag :li, class: "filter_icon #{active}" do + link_to request.path, link_opts do + icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip) + end + end + end + + def icon_for_event + { + EventFilter.push => 'upload', + EventFilter.merged => 'check-square-o', + EventFilter.comments => 'comments', + EventFilter.team => 'user', + } + end + + def event_feed_title(event) + words = [] + words << event.author_name + words << event_action_name(event) + + if event.push? + words << event.ref_type + words << event.ref_name + words << "at" + elsif event.commented? + if event.note_commit? + words << event.note_short_commit_id + else + words << "##{truncate event.note_target_iid}" + end + words << "at" + elsif event.target + words << "##{event.target_iid}:" + words << event.target.title if event.target.respond_to?(:title) + words << "at" + end + + words << event.project_name + + words.join(" ") + end + + def event_feed_url(event) + if event.issue? + namespace_project_issue_url(event.project.namespace, event.project, + event.issue) + elsif event.merge_request? + namespace_project_merge_request_url(event.project.namespace, + event.project, event.merge_request) + elsif event.note? && event.note_commit? + namespace_project_commit_url(event.project.namespace, event.project, + event.note_target) + elsif event.note? + if event.note_target + if event.note_commit? + namespace_project_commit_path(event.project.namespace, event.project, + event.note_commit_id, + anchor: dom_id(event.target)) + elsif event.note_project_snippet? + namespace_project_snippet_path(event.project.namespace, + event.project, event.note_target) + else + event_note_target_path(event) + end + end + elsif event.push? + if event.push_with_commits? && event.md_ref? + if event.commits_count > 1 + namespace_project_compare_url(event.project.namespace, event.project, + from: event.commit_from, to: + event.commit_to) + else + namespace_project_commit_url(event.project.namespace, event.project, + id: event.commit_to) + end + else + namespace_project_commits_url(event.project.namespace, event.project, + event.ref_name) + end + end + end + + def event_feed_summary(event) + if event.issue? + render "events/event_issue", issue: event.issue + elsif event.push? + render "events/event_push", event: event + elsif event.merge_request? + render "events/event_merge_request", merge_request: event.merge_request + elsif event.note? + render "events/event_note", note: event.note + end + end + + def event_note_target_path(event) + if event.note? && event.note_commit? + namespace_project_commit_path(event.project.namespace, event.project, + event.note_target) + else + polymorphic_path([event.project.namespace.becomes(Namespace), + event.project, event.note_target], + anchor: dom_id(event.target)) + end + end + + def event_note_title_html(event) + if event.note_target + if event.note_commit? + link_to( + namespace_project_commit_path(event.project.namespace, event.project, + event.note_commit_id, + anchor: dom_id(event.target)), + class: "commit_short_id" + ) do + "#{event.note_target_type} #{event.note_short_commit_id}" + end + elsif event.note_project_snippet? + link_to(namespace_project_snippet_path(event.project.namespace, + event.project, + event.note_target)) do + "#{event.note_target_type} ##{truncate event.note_target_id}" + end + else + link_to event_note_target_path(event) do + "#{event.note_target_type} ##{truncate event.note_target_iid}" + end + end + else + content_tag :strong do + "(deleted)" + end + end + end + + def event_note(text, options = {}) + text = first_line_in_markdown(text, 150, options) + sanitize(text, tags: %w(a img b pre code p span)) + end + + def event_commit_title(message) + escape_once(truncate(message.split("\n").first, length: 70)) + rescue + "--broken encoding" + end + + def event_to_atom(xml, event) + if event.proper? + xml.entry do + event_link = event_feed_url(event) + event_title = event_feed_title(event) + event_summary = event_feed_summary(event) + + xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" + xml.link href: event_link + xml.title truncate(event_title, length: 80) + xml.updated event.created_at.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(event.author_email) + xml.author do |author| + xml.name event.author_name + xml.email event.author_email + end + + xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? } + end + end + end + end +end diff --git a/app/helpers/gitlab/explore_helper.rb b/app/helpers/gitlab/explore_helper.rb new file mode 100644 index 00000000000..b8e0f482b94 --- /dev/null +++ b/app/helpers/gitlab/explore_helper.rb @@ -0,0 +1,19 @@ +module Gitlab + module ExploreHelper + def explore_projects_filter_path(options={}) + exist_opts = { + sort: params[:sort], + scope: params[:scope], + group: params[:group], + tag: params[:tag], + visibility_level: params[:visibility_level], + } + + options = exist_opts.merge(options) + + path = explore_projects_path + path << "?#{options.to_param}" + path + end + end +end diff --git a/app/helpers/gitlab/external_wiki_helper.rb b/app/helpers/gitlab/external_wiki_helper.rb new file mode 100644 index 00000000000..710cdc727d0 --- /dev/null +++ b/app/helpers/gitlab/external_wiki_helper.rb @@ -0,0 +1,13 @@ +module Gitlab + module ExternalWikiHelper + def get_project_wiki_path(project) + external_wiki_service = project.services. + select { |service| service.to_param == 'external_wiki' }.first + if external_wiki_service.present? && external_wiki_service.active? + external_wiki_service.properties['external_wiki_url'] + else + namespace_project_wiki_path(project.namespace, project, :home) + end + end + end +end diff --git a/app/helpers/gitlab/git_helper.rb b/app/helpers/gitlab/git_helper.rb new file mode 100644 index 00000000000..867b30b8c74 --- /dev/null +++ b/app/helpers/gitlab/git_helper.rb @@ -0,0 +1,7 @@ +module Gitlab + module GitHelper + def strip_gpg_signature(text) + text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "") + end + end +end diff --git a/app/helpers/gitlab/gitlab_markdown_helper.rb b/app/helpers/gitlab/gitlab_markdown_helper.rb new file mode 100644 index 00000000000..265cb4672fe --- /dev/null +++ b/app/helpers/gitlab/gitlab_markdown_helper.rb @@ -0,0 +1,195 @@ +require 'nokogiri' + +module Gitlab + module GitlabMarkdownHelper + include Gitlab::Markdown + include PreferencesHelper + + # Use this in places where you would normally use link_to(gfm(...), ...). + # + # It solves a problem occurring with nested links (i.e. + # "outer text gfm ref more outer text"). This will not be + # interpreted as intended. Browsers will parse something like + # "outer text gfm ref more outer text" (notice the last part is + # not linked any more). link_to_gfm corrects that. It wraps all parts to + # explicitly produce the correct linking behavior (i.e. + # "outer text gfm ref more outer text"). + def link_to_gfm(body, url, html_options = {}) + return "" if body.blank? + + escaped_body = if body =~ /\A\ at the beginning of a line", + "Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___" + ].freeze + + # Returns a random markdown tip for use as a textarea placeholder + def random_markdown_tip + MARKDOWN_TIPS.sample + end + + private + + # Return +text+, truncated to +max_chars+ characters, excluding any HTML + # tags. + def truncate_visible(text, max_chars) + doc = Nokogiri::HTML.fragment(text) + content_length = 0 + truncated = false + + doc.traverse do |node| + if node.text? || node.content.empty? + if truncated + node.remove + next + end + + # Handle line breaks within a node + if node.content.strip.lines.length > 1 + node.content = "#{node.content.lines.first.chomp}..." + truncated = true + end + + num_remaining = max_chars - content_length + if node.content.length > num_remaining + node.content = node.content.truncate(num_remaining) + truncated = true + end + content_length += node.content.length + end + + truncated = truncate_if_block(node, truncated) + end + + doc.to_html + end + + # Used by #truncate_visible. If +node+ is the first block element, and the + # text hasn't already been truncated, then append "..." to the node contents + # and return true. Otherwise return false. + def truncate_if_block(node, truncated) + if node.element? && node.description.block? && !truncated + node.content = "#{node.content}..." if node.next_sibling + true + else + truncated + end + end + + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String + def cross_project_reference(project, entity) + if entity.respond_to?(:to_reference) + "#{project.to_reference}#{entity.to_reference}" + else + '' + end + end + end +end diff --git a/app/helpers/gitlab/gitlab_routing_helper.rb b/app/helpers/gitlab/gitlab_routing_helper.rb new file mode 100644 index 00000000000..7f1e455d5de --- /dev/null +++ b/app/helpers/gitlab/gitlab_routing_helper.rb @@ -0,0 +1,69 @@ +# Shorter routing method for project and project items +# Since update to rails 4.1.9 we are now allowed to use `/` in project routing +# so we use nested routing for project resources which include project and +# project namespace. To avoid writing long methods every time we define shortcuts for +# some of routing. +# +# For example instead of this: +# +# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request) +# +# We can simply use shortcut: +# +# merge_request_path(merge_request) +# +module Gitlab + module GitlabRoutingHelper + def project_path(project, *args) + namespace_project_path(project.namespace, project, *args) + end + + def activity_project_path(project, *args) + 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 issue_path(entity, *args) + namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) + end + + def merge_request_path(entity, *args) + namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) + end + + def milestone_path(entity, *args) + 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 + + def merge_request_url(entity, *args) + namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) + end + + def project_snippet_url(entity, *args) + namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) + end + + def toggle_subscription_path(entity, *args) + if entity.is_a?(Issue) + toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) + else + toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity) + end + end + end +end diff --git a/app/helpers/gitlab/graph_helper.rb b/app/helpers/gitlab/graph_helper.rb new file mode 100644 index 00000000000..047f5c19095 --- /dev/null +++ b/app/helpers/gitlab/graph_helper.rb @@ -0,0 +1,18 @@ +module Gitlab + module GraphHelper + def get_refs(repo, commit) + refs = "" + refs << commit.ref_names(repo).join(' ') + + # append note count + refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 + + refs + end + + def parents_zip_spaces(parents, parent_spaces) + ids = parents.map { |p| p.id } + ids.zip(parent_spaces) + end + end +end diff --git a/app/helpers/gitlab/groups_helper.rb b/app/helpers/gitlab/groups_helper.rb new file mode 100644 index 00000000000..8172c617249 --- /dev/null +++ b/app/helpers/gitlab/groups_helper.rb @@ -0,0 +1,35 @@ +module Gitlab + 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 group_icon(group) + if group.is_a?(String) + group = Group.find_by(path: group) + end + + if group && group.avatar.present? + group.avatar.url + else + image_path('no_group_avatar.png') + end + end + end +end diff --git a/app/helpers/gitlab/icons_helper.rb b/app/helpers/gitlab/icons_helper.rb new file mode 100644 index 00000000000..e815d237bb1 --- /dev/null +++ b/app/helpers/gitlab/icons_helper.rb @@ -0,0 +1,87 @@ +module Gitlab + module IconsHelper + include FontAwesome::Rails::IconHelper + + # Creates an icon tag given icon name(s) and possible icon modifiers. + # + # Right now this method simply delegates directly to `fa_icon` from the + # font-awesome-rails gem, but should we ever use a different icon pack in the + # future we won't have to change hundreds of method calls. + def icon(names, options = {}) + fa_icon(names, options) + end + + def spinner(text = nil, visible = false) + css_class = 'loading' + css_class << ' hide' unless visible + + content_tag :div, class: css_class do + icon('spinner spin') + text + end + end + + def boolean_to_icon(value) + if value + icon('circle', class: 'cgreen') + else + icon('power-off', class: 'clgray') + end + end + + def public_icon + icon('globe fw') + end + + def internal_icon + icon('shield fw') + end + + def private_icon + icon('lock fw') + end + + def file_type_icon_class(type, mode, name) + if type == 'folder' + icon_class = 'folder' + elsif mode == '120000' + icon_class = 'share' + else + # Guess which icon to choose based on file extension. + # If you think a file extension is missing, feel free to add it on PR + + case File.extname(name).downcase + when '.pdf' + icon_class = 'file-pdf-o' + when '.jpg', '.jpeg', '.jif', '.jfif', + '.jp2', '.jpx', '.j2k', '.j2c', + '.png', '.gif', '.tif', '.tiff', + '.svg', '.ico', '.bmp' + icon_class = 'file-image-o' + when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip', + '.xz', '.rar', '.7z' + icon_class = 'file-archive-o' + when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac' + icon_class = 'file-audio-o' + when '.mp4', '.m4p', '.m4v', + '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv', + '.mpg', '.mpeg', '.m2v', + '.avi', '.mkv', '.flv', '.ogv', '.mov', + '.3gp', '.3g2' + icon_class = 'file-video-o' + when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb' + icon_class = 'file-word-o' + when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm', + '.xlsb', '.xla', '.xlam', '.xll', '.xlw' + icon_class = 'file-excel-o' + when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm', + '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm' + icon_class = 'file-powerpoint-o' + else + icon_class = 'file-text-o' + end + end + + icon_class + end + end +end diff --git a/app/helpers/gitlab/issues_helper.rb b/app/helpers/gitlab/issues_helper.rb new file mode 100644 index 00000000000..67238926555 --- /dev/null +++ b/app/helpers/gitlab/issues_helper.rb @@ -0,0 +1,90 @@ +module Gitlab + module IssuesHelper + def issue_css_classes(issue) + classes = "issue" + classes << " closed" if issue.closed? + classes << " today" if issue.today? + classes + end + + # Returns an OpenStruct object suitable for use by options_from_collection_for_select + # to allow filtering issues by an unassigned User or Milestone + def unassigned_filter + # Milestone uses :title, Issue uses :name + OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') + end + + def url_for_project_issues(project = @project, options = {}) + return '' if project.nil? + + if options[:only_path] + project.issues_tracker.project_path + else + project.issues_tracker.project_url + end + end + + def url_for_new_issue(project = @project, options = {}) + return '' if project.nil? + + if options[:only_path] + project.issues_tracker.new_issue_path + else + project.issues_tracker.new_issue_url + end + end + + def url_for_issue(issue_iid, project = @project, options = {}) + return '' if project.nil? + + if options[:only_path] + project.issues_tracker.issue_path(issue_iid) + else + project.issues_tracker.issue_url(issue_iid) + end + end + + def bulk_update_milestone_options + options_for_select([['None (backlog)', -1]]) + + options_from_collection_for_select(project_active_milestones, 'id', + 'title', params[:milestone_id]) + end + + def milestone_options(object) + options_from_collection_for_select(object.project.milestones.active, + 'id', 'title', object.milestone_id) + end + + def issue_box_class(item) + if item.respond_to?(:expired?) && item.expired? + 'issue-box-expired' + elsif item.respond_to?(:merged?) && item.merged? + 'issue-box-merged' + elsif item.closed? + 'issue-box-closed' + else + 'issue-box-open' + end + end + + def issue_to_atom(xml, issue) + xml.entry do + xml.id namespace_project_issue_url(issue.project.namespace, + issue.project, issue) + xml.link href: namespace_project_issue_url(issue.project.namespace, + issue.project, issue) + xml.title truncate(issue.title, length: 80) + xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email) + xml.author do |author| + xml.name issue.author_name + xml.email issue.author_email + end + xml.summary issue.title + end + end + + # Required for Gitlab::Markdown::IssueReferenceFilter + module_function :url_for_issue + end +end diff --git a/app/helpers/gitlab/labels_helper.rb b/app/helpers/gitlab/labels_helper.rb new file mode 100644 index 00000000000..aa16d71f42c --- /dev/null +++ b/app/helpers/gitlab/labels_helper.rb @@ -0,0 +1,103 @@ +module Gitlab + module LabelsHelper + include ActionView::Helpers::TagHelper + + # Link to a Label + # + # label - Label object to link to + # project - Project object which will be used as the context for the label's + # link. If omitted, defaults to `@project`, or the label's own + # project. + # block - An optional block that will be passed to `link_to`, forming the + # body of the link element. If omitted, defaults to + # `render_colored_label`. + # + # Examples: + # + # # Allow the generated link to use the label's own project + # link_to_label(label) + # + # # Force the generated link to use @project + # @project = Project.first + # link_to_label(label) + # + # # Force the generated link to use a provided project + # link_to_label(label, project: Project.last) + # + # # Customize link body with a block + # link_to_label(label) { "My Custom Label Text" } + # + # Returns a String + def link_to_label(label, project: nil, &block) + project ||= @project || label.project + link = namespace_project_issues_path(project.namespace, project, + label_name: label.name) + + if block_given? + link_to link, &block + else + link_to render_colored_label(label), link + end + end + + def project_label_names + @project.labels.pluck(:title) + end + + def render_colored_label(label) + label_color = label.color || Label::DEFAULT_COLOR + text_color = text_color_for_bg(label_color) + + # Intentionally not using content_tag here so that this method can be called + # by LabelReferenceFilter + span = %() + + escape_once(label.name) + '' + + span.html_safe + end + + def suggested_colors + [ + '#0033CC', + '#428BCA', + '#44AD8E', + '#A8D695', + '#5CB85C', + '#69D100', + '#004E00', + '#34495E', + '#7F8C8D', + '#A295D6', + '#5843AD', + '#8E44AD', + '#FFECDB', + '#AD4363', + '#D10069', + '#CC0033', + '#FF0000', + '#D9534F', + '#D1D100', + '#F0AD4E', + '#AD8D43' + ] + end + + def text_color_for_bg(bg_color) + r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex) + + if (r + g + b) > 500 + '#333333' + else + '#FFFFFF' + end + end + + def project_labels_options(project) + options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) + end + + # Required for Gitlab::Markdown::LabelReferenceFilter + module_function :render_colored_label, :text_color_for_bg, :escape_once + end +end diff --git a/app/helpers/gitlab/merge_requests_helper.rb b/app/helpers/gitlab/merge_requests_helper.rb new file mode 100644 index 00000000000..361f6b2fdac --- /dev/null +++ b/app/helpers/gitlab/merge_requests_helper.rb @@ -0,0 +1,76 @@ +module Gitlab + module MergeRequestsHelper + def new_mr_path_from_push_event(event) + target_project = event.project.forked_from_project || event.project + new_namespace_project_merge_request_path( + event.project.namespace, + event.project, + new_mr_from_push_event(event, target_project) + ) + end + + def new_mr_path_for_fork_from_push_event(event) + new_namespace_project_merge_request_path( + event.project.namespace, + event.project, + new_mr_from_push_event(event, event.project.forked_from_project) + ) + end + + def new_mr_from_push_event(event, target_project) + { + merge_request: { + source_project_id: event.project.id, + target_project_id: target_project.id, + source_branch: event.branch_name, + target_branch: target_project.repository.root_ref + } + } + end + + def mr_css_classes(mr) + classes = "merge-request" + classes << " closed" if mr.closed? + classes << " merged" if mr.merged? + classes + end + + def ci_build_details_path(merge_request) + merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) + end + + def merge_path_description(merge_request, separator) + if merge_request.for_fork? + "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}" + else + "Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}" + end + end + + def issues_sentence(issues) + issues.map { |i| "##{i.iid}" }.to_sentence + end + + def mr_change_branches_path(merge_request) + new_namespace_project_merge_request_path( + @project.namespace, @project, + merge_request: { + source_project_id: @merge_request.source_project_id, + target_project_id: @merge_request.target_project_id, + source_branch: @merge_request.source_branch, + target_branch: nil + } + ) + end + + def source_branch_with_namespace(merge_request) + if merge_request.for_fork? + namespace = link_to(merge_request.source_project_namespace, + project_path(merge_request.source_project)) + namespace + ":#{merge_request.source_branch}" + else + merge_request.source_branch + end + end + end +end diff --git a/app/helpers/gitlab/milestones_helper.rb b/app/helpers/gitlab/milestones_helper.rb new file mode 100644 index 00000000000..116967d4946 --- /dev/null +++ b/app/helpers/gitlab/milestones_helper.rb @@ -0,0 +1,38 @@ +module Gitlab + module MilestonesHelper + def milestones_filter_path(opts = {}) + if @project + namespace_project_milestones_path(@project.namespace, @project, opts) + elsif @group + group_milestones_path(@group, opts) + else + dashboard_milestones_path(opts) + end + end + + def milestone_progress_bar(milestone) + options = { + class: 'progress-bar progress-bar-success', + style: "width: #{milestone.percent_complete}%;" + } + + content_tag :div, class: 'progress' do + content_tag :div, nil, options + end + end + + def projects_milestones_options + milestones = + if @project + @project.milestones + else + Milestone.where(project_id: @projects) + end.active + + grouped_milestones = Milestones::GroupService.new(milestones).execute + grouped_milestones.unshift(Milestone::None) + + options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title]) + end + end +end diff --git a/app/helpers/gitlab/namespaces_helper.rb b/app/helpers/gitlab/namespaces_helper.rb new file mode 100644 index 00000000000..b1caaac3f63 --- /dev/null +++ b/app/helpers/gitlab/namespaces_helper.rb @@ -0,0 +1,38 @@ +module Gitlab + module NamespacesHelper + def namespaces_options(selected = :current_user, scope = :default) + groups = current_user.owned_groups + current_user.masters_groups + users = [current_user.namespace] + + group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ] + users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ] + + options = [] + options << group_opts + options << users_opts + + if selected == :current_user && current_user.namespace + selected = current_user.namespace.id + end + + grouped_options_for_select(options, selected) + end + + def namespace_select_tag(id, opts = {}) + css_class = "ajax-namespace-select " + css_class << "multiselect " if opts[:multiple] + css_class << (opts[:class] || '') + value = opts[:selected] || '' + + hidden_field_tag(id, value, class: css_class) + end + + def namespace_icon(namespace, size = 40) + if namespace.kind_of?(Group) + group_icon(namespace) + else + avatar_icon(namespace.owner.email, size) + end + end + end +end diff --git a/app/helpers/gitlab/nav_helper.rb b/app/helpers/gitlab/nav_helper.rb new file mode 100644 index 00000000000..14106d70840 --- /dev/null +++ b/app/helpers/gitlab/nav_helper.rb @@ -0,0 +1,23 @@ +module Gitlab + module NavHelper + def nav_menu_collapsed? + cookies[:collapsed_nav] == 'true' + end + + def nav_sidebar_class + if nav_menu_collapsed? + "page-sidebar-collapsed" + else + "page-sidebar-expanded" + end + end + + def nav_header_class + if nav_menu_collapsed? + "header-collapsed" + else + "header-expanded" + end + end + end +end diff --git a/app/helpers/gitlab/notes_helper.rb b/app/helpers/gitlab/notes_helper.rb new file mode 100644 index 00000000000..15076148b02 --- /dev/null +++ b/app/helpers/gitlab/notes_helper.rb @@ -0,0 +1,78 @@ +module Gitlab + module NotesHelper + # Helps to distinguish e.g. commit notes in mr notes list + def note_for_main_target?(note) + (@noteable.class.name == note.noteable_type && !note.for_diff_line?) + end + + def note_target_fields(note) + hidden_field_tag(:target_type, note.noteable.class.name.underscore) + + hidden_field_tag(:target_id, note.noteable.id) + end + + def note_editable?(note) + note.editable? && can?(current_user, :admin_note, note) + end + + def link_to_commit_diff_line_note(note) + if note.for_commit_diff_line? + link_to( + "#{note.diff_file_name}:L#{note.diff_new_line}", + namespace_project_commit_path(@project.namespace, @project, + note.noteable, anchor: note.line_code) + ) + end + end + + def noteable_json(noteable) + { + id: noteable.id, + class: noteable.class.name, + resources: noteable.class.table_name, + project_id: noteable.project.id, + }.to_json + end + + def link_to_new_diff_note(line_code, line_type = nil) + discussion_id = Note.build_discussion_id( + @comments_target[:noteable_type], + @comments_target[:noteable_id] || @comments_target[:commit_id], + line_code + ) + + data = { + noteable_type: @comments_target[:noteable_type], + noteable_id: @comments_target[:noteable_id], + commit_id: @comments_target[:commit_id], + line_code: line_code, + discussion_id: discussion_id, + line_type: line_type + } + + button_tag(class: 'btn add-diff-note js-add-diff-note-button', + data: data, + title: 'Add a comment to this line') do + icon('comment-o') + end + end + + def link_to_reply_diff(note, line_type = nil) + return unless current_user + + data = { + noteable_type: note.noteable_type, + noteable_id: note.noteable_id, + commit_id: note.commit_id, + line_code: note.line_code, + discussion_id: note.discussion_id, + line_type: line_type + } + + button_tag class: 'btn reply-btn js-discussion-reply-button', + data: data, title: 'Add a reply' do + link_text = icon('comment') + link_text << ' Reply' + end + end + end +end diff --git a/app/helpers/gitlab/notifications_helper.rb b/app/helpers/gitlab/notifications_helper.rb new file mode 100644 index 00000000000..b6324044ab1 --- /dev/null +++ b/app/helpers/gitlab/notifications_helper.rb @@ -0,0 +1,17 @@ +module Gitlab + module NotificationsHelper + include IconsHelper + + def notification_icon(notification) + if notification.disabled? + icon('volume-off', class: 'ns-mute') + elsif notification.participating? + icon('volume-down', class: 'ns-part') + elsif notification.watch? + icon('volume-up', class: 'ns-watch') + else + icon('circle-o', class: 'ns-default') + end + end + end +end diff --git a/app/helpers/gitlab/page_layout_helper.rb b/app/helpers/gitlab/page_layout_helper.rb new file mode 100644 index 00000000000..d7a85186155 --- /dev/null +++ b/app/helpers/gitlab/page_layout_helper.rb @@ -0,0 +1,28 @@ +module Gitlab + module PageLayoutHelper + def page_title(*titles) + @page_title ||= [] + + @page_title.push(*titles.compact) if titles.any? + + @page_title.join(" | ") + end + + def header_title(title = nil, title_url = nil) + if title + @header_title = title + @header_title_url = title_url + else + @header_title_url ? link_to(@header_title, @header_title_url) : @header_title + end + end + + def sidebar(name = nil) + if name + @sidebar = name + else + @sidebar + end + end + end +end diff --git a/app/helpers/gitlab/preferences_helper.rb b/app/helpers/gitlab/preferences_helper.rb new file mode 100644 index 00000000000..3eac5d51acd --- /dev/null +++ b/app/helpers/gitlab/preferences_helper.rb @@ -0,0 +1,67 @@ +module Gitlab + # Helper methods for per-User preferences + module PreferencesHelper + COLOR_SCHEMES = { + 1 => 'white', + 2 => 'dark', + 3 => 'solarized-light', + 4 => 'solarized-dark', + 5 => 'monokai', + } + COLOR_SCHEMES.default = 'white' + + # Helper method to access the COLOR_SCHEMES + # + # The keys are the `color_scheme_ids` + # The values are the `name` of the scheme. + # + # The preview images are `name-scheme-preview.png` + # The stylesheets should use the css class `.name` + def color_schemes + COLOR_SCHEMES.freeze + end + + # Maps `dashboard` values to more user-friendly option text + DASHBOARD_CHOICES = { + projects: 'Your Projects (default)', + stars: 'Starred Projects' + }.with_indifferent_access.freeze + + # Returns an Array usable by a select field for more user-friendly option text + def dashboard_choices + defined = User.dashboards + + if defined.size != DASHBOARD_CHOICES.size + # Ensure that anyone adding new options updates this method too + raise RuntimeError, "`User` defines #{defined.size} dashboard choices," + + " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." + else + defined.map do |key, _| + # Use `fetch` so `KeyError` gets raised when a key is missing + [DASHBOARD_CHOICES.fetch(key), key] + end + end + end + + def project_view_choices + [ + ['Readme (default)', :readme], + ['Activity view', :activity] + ] + end + + def user_application_theme + theme = Gitlab::Themes.by_id(current_user.try(:theme_id)) + theme.css_class + end + + def user_color_scheme_class + COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) + end + + def prefer_readme? + !current_user || + current_user.project_view == 'readme' + end + end +end diff --git a/app/helpers/gitlab/projects_helper.rb b/app/helpers/gitlab/projects_helper.rb new file mode 100644 index 00000000000..8a8cd6048df --- /dev/null +++ b/app/helpers/gitlab/projects_helper.rb @@ -0,0 +1,332 @@ +module Gitlab + 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] do + title = content_tag(:span, project.name, class: 'project-name') + + if project.namespace + namespace = content_tag(:span, "#{project.namespace.human_name} / ", class: 'namespace-name') + title = namespace + title + end + + title + end + end + + def link_to_member(project, author, opts = {}) + default_opts = { avatar: true, name: true, size: 16, author_class: 'author' } + opts = default_opts.merge(opts) + + return "(deleted)" unless author + + author_html = "" + + # Build avatar image tag + author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] + + # Build name span tag + author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] + + author_html = author_html.html_safe + + if opts[:name] + link_to(author_html, user_path(author), class: "author_link").html_safe + else + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe + end + end + + def project_title(project) + if project.group + content_tag :span do + link_to( + simple_sanitize(project.group.name), group_path(project.group) + ) + ' / ' + + link_to(simple_sanitize(project.name), + project_path(project)) + end + else + owner = project.namespace.owner + content_tag :span do + link_to( + simple_sanitize(owner.name), user_path(owner) + ) + ' / ' + + link_to(simple_sanitize(project.name), + project_path(project)) + end + end + end + + def remove_project_message(project) + "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?" + end + + def transfer_project_message(project) + "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?" + end + + def project_nav_tabs + @nav_tabs ||= get_project_nav_tabs(@project, current_user) + end + + def project_nav_tab?(name) + project_nav_tabs.include? name + end + + def project_active_milestones + @project.milestones.active.order("due_date, title ASC") + end + + def project_for_deploy_key(deploy_key) + if deploy_key.projects.include?(@project) + @project + else + deploy_key.projects.find { |project| can?(current_user, :read_project, project) } + end + end + + def can_change_visibility_level?(project, current_user) + return false unless can?(current_user, :change_visibility_level, project) + + if project.forked? + project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE + else + true + end + end + + private + + def get_project_nav_tabs(project, current_user) + nav_tabs = [:home] + + if !project.empty_repo? && can?(current_user, :download_code, project) + nav_tabs << [:files, :commits, :network, :graphs] + end + + if project.repo_exists? && can?(current_user, :read_merge_request, project) + nav_tabs << :merge_requests + end + + if can?(current_user, :admin_project, project) + nav_tabs << :settings + end + + if can?(current_user, :read_issue, project) + nav_tabs << :issues + end + + if can?(current_user, :read_wiki, project) + nav_tabs << :wiki + end + + if can?(current_user, :read_project_snippet, project) + nav_tabs << :snippets + end + + if can?(current_user, :read_label, project) + nav_tabs << :labels + end + + if can?(current_user, :read_milestone, project) + nav_tabs << :milestones + end + + nav_tabs.flatten + end + + def git_user_name + if current_user + current_user.name + else + "Your name" + end + end + + def git_user_email + if current_user + current_user.email + else + "your@email.com" + end + end + + def repository_size(project = nil) + "#{(project || @project).repository_size} MB" + rescue + # In order to prevent 500 error + # when application cannot allocate memory + # to calculate repo size - just show 'Unknown' + 'unknown' + end + + def default_url_to_repo(project = nil) + project = project || @project + current_user ? project.url_to_repo : project.http_url_to_repo + end + + def default_clone_protocol + current_user ? "ssh" : "http" + end + + def project_last_activity(project) + if project.last_activity_at + time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago') + else + "Never" + end + end + + def add_contribution_guide_path(project) + if project && !project.repository.contribution_guide + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "CONTRIBUTING.md", + commit_message: "Add contribution guide" + ) + end + end + + def add_changelog_path(project) + if project && !project.repository.changelog + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "CHANGELOG", + commit_message: "Add changelog" + ) + end + end + + def add_license_path(project) + if project && !project.repository.license + namespace_project_new_blob_path( + project.namespace, + project, + project.default_branch, + file_name: "LICENSE", + commit_message: "Add license" + ) + end + end + + def contribution_guide_path(project) + if project && contribution_guide = project.repository.contribution_guide + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + contribution_guide.name) + ) + end + end + + def readme_path(project) + filename_path(project, :readme) + end + + def changelog_path(project) + filename_path(project, :changelog) + end + + def license_path(project) + filename_path(project, :license) + end + + def version_path(project) + filename_path(project, :version) + end + + def hidden_pass_url(original_url) + result = URI(original_url) + result.password = '*****' unless result.password.nil? + result + rescue + original_url + end + + def project_wiki_path_with_version(proj, page, version, is_newest) + url_params = is_newest ? {} : { version_id: version } + namespace_project_wiki_path(proj.namespace, proj, page, url_params) + end + + def project_status_css_class(status) + case status + when "started" + "active" + when "failed" + "danger" + when "finished" + "success" + end + end + + def user_max_access_in_project(user, project) + level = project.team.max_member_access(user) + + if level + Gitlab::Access.options_with_owner.key(level) + 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' + + namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') + end + + def last_push_event + if current_user + current_user.recent_push(@project.id) + end + end + + def readme_cache_key + sha = @project.commit.try(:sha) || 'nil' + [@project.id, sha, "readme"].join('-') + end + + def round_commit_count(project) + count = project.commit_count + + if count > 10000 + '10000+' + elsif count > 5000 + '5000+' + elsif count > 1000 + '1000+' + else + count + end + end + + private + + def filename_path(project, filename) + if project && blob = project.repository.send(filename) + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + blob.name) + ) + end + end + end +end diff --git a/app/helpers/gitlab/search_helper.rb b/app/helpers/gitlab/search_helper.rb new file mode 100644 index 00000000000..f9caf8f2431 --- /dev/null +++ b/app/helpers/gitlab/search_helper.rb @@ -0,0 +1,114 @@ +module Gitlab + module SearchHelper + def search_autocomplete_opts(term) + return unless current_user + + resources_results = [ + groups_autocomplete(term), + projects_autocomplete(term) + ].flatten + + generic_results = project_autocomplete + default_autocomplete + help_autocomplete + generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") } + + [ + resources_results, + generic_results + ].flatten.uniq do |item| + item[:label] + end + end + + private + + # Autocomplete results for various settings pages + def default_autocomplete + [ + { label: "Profile settings", url: profile_path }, + { label: "SSH Keys", url: profile_keys_path }, + { label: "Dashboard", url: root_path }, + { label: "Admin Section", url: admin_root_path }, + ] + end + + # Autocomplete results for internal help pages + def help_autocomplete + [ + { label: "help: API Help", url: help_page_path("api", "README") }, + { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, + { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, + { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, + { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, + { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, + { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, + { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, + ] + end + + # Autocomplete results for the current project, if it's defined + def project_autocomplete + if @project && @project.repository.exists? && @project.repository.root_ref + prefix = search_result_sanitize(@project.name_with_namespace) + ref = @ref || @project.repository.root_ref + + [ + { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + ] + else + [] + end + end + + # Autocomplete results for the current user's groups + def groups_autocomplete(term, limit = 5) + current_user.authorized_groups.search(term).limit(limit).map do |group| + { + label: "group: #{search_result_sanitize(group.name)}", + url: group_path(group) + } + end + end + + # Autocomplete results for the current user's projects + def projects_autocomplete(term, limit = 5) + ProjectsFinder.new.execute(current_user).search_by_title(term). + sorted_by_stars.non_archived.limit(limit).map do |p| + { + label: "project: #{search_result_sanitize(p.name_with_namespace)}", + url: namespace_project_path(p.namespace, p) + } + end + end + + def search_result_sanitize(str) + Sanitize.clean(str) + end + + def search_filter_path(options={}) + exist_opts = { + search: params[:search], + project_id: params[:project_id], + group_id: params[:group_id], + scope: params[:scope] + } + + options = exist_opts.merge(options) + search_path(options) + end + + # Sanitize html generated after parsing markdown from issue description or comment + def search_md_sanitize(html) + sanitize(html, tags: %w(a p ol ul li pre code)) + end + end +end diff --git a/app/helpers/gitlab/selects_helper.rb b/app/helpers/gitlab/selects_helper.rb new file mode 100644 index 00000000000..d52d670a1cf --- /dev/null +++ b/app/helpers/gitlab/selects_helper.rb @@ -0,0 +1,47 @@ +module Gitlab + module SelectsHelper + def users_select_tag(id, opts = {}) + css_class = "ajax-users-select " + css_class << "multiselect " if opts[:multiple] + css_class << (opts[:class] || '') + value = opts[:selected] || '' + placeholder = opts[:placeholder] || 'Search for a user' + + null_user = opts[:null_user] || false + any_user = opts[:any_user] || false + email_user = opts[:email_user] || false + first_user = opts[:first_user] && current_user ? current_user.username : false + current_user = opts[:current_user] || false + project = opts[:project] || @project + + html = { + class: css_class, + 'data-placeholder' => placeholder, + 'data-null-user' => null_user, + 'data-any-user' => any_user, + 'data-email-user' => email_user, + 'data-first-user' => first_user, + 'data-current-user' => current_user + } + + unless opts[:scope] == :all + if project + html['data-project-id'] = project.id + elsif @group + html['data-group-id'] = @group.id + end + end + + hidden_field_tag(id, value, html) + end + + def groups_select_tag(id, opts = {}) + css_class = "ajax-groups-select " + css_class << "multiselect " if opts[:multiple] + css_class << (opts[:class] || '') + value = opts[:selected] || '' + + hidden_field_tag(id, value, class: css_class) + end + end +end diff --git a/app/helpers/gitlab/snippets_helper.rb b/app/helpers/gitlab/snippets_helper.rb new file mode 100644 index 00000000000..aaf4d43f852 --- /dev/null +++ b/app/helpers/gitlab/snippets_helper.rb @@ -0,0 +1,22 @@ +module Gitlab + module SnippetsHelper + def lifetime_select_options + options = [ + ['forever', nil], + ['1 day', "#{Date.current + 1.day}"], + ['1 week', "#{Date.current + 1.week}"], + ['1 month', "#{Date.current + 1.month}"] + ] + options_for_select(options) + end + + def reliable_snippet_path(snippet) + if snippet.project_id? + namespace_project_snippet_path(snippet.project.namespace, + snippet.project, snippet) + else + snippet_path(snippet) + end + end + end +end diff --git a/app/helpers/gitlab/sorting_helper.rb b/app/helpers/gitlab/sorting_helper.rb new file mode 100644 index 00000000000..29c63a0d129 --- /dev/null +++ b/app/helpers/gitlab/sorting_helper.rb @@ -0,0 +1,98 @@ +module Gitlab + module SortingHelper + def sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_milestone_soon => sort_title_milestone_soon, + sort_value_milestone_later => sort_title_milestone_later, + sort_value_largest_repo => sort_title_largest_repo, + sort_value_recently_signin => sort_title_recently_signin, + sort_value_oldest_signin => sort_title_oldest_signin, + } + end + + def sort_title_oldest_updated + 'Oldest updated' + end + + def sort_title_recently_updated + 'Recently updated' + end + + def sort_title_oldest_created + 'Oldest created' + end + + def sort_title_recently_created + 'Recently created' + end + + def sort_title_milestone_soon + 'Milestone due soon' + end + + def sort_title_milestone_later + 'Milestone due later' + end + + def sort_title_name + 'Name' + end + + def sort_title_largest_repo + 'Largest repository' + end + + def sort_title_recently_signin + 'Recent sign in' + end + + def sort_title_oldest_signin + 'Oldest sign in' + end + + def sort_value_oldest_updated + 'updated_asc' + end + + def sort_value_recently_updated + 'updated_desc' + end + + def sort_value_oldest_created + 'created_asc' + end + + def sort_value_recently_created + 'created_desc' + end + + def sort_value_milestone_soon + 'milestone_due_asc' + end + + def sort_value_milestone_later + 'milestone_due_desc' + end + + def sort_value_name + 'name_asc' + end + + def sort_value_largest_repo + 'repository_size_desc' + end + + def sort_value_recently_signin + 'recent_sign_in' + end + + def sort_value_oldest_signin + 'oldest_sign_in' + end + end +end diff --git a/app/helpers/gitlab/submodule_helper.rb b/app/helpers/gitlab/submodule_helper.rb new file mode 100644 index 00000000000..c0fbebcb1d9 --- /dev/null +++ b/app/helpers/gitlab/submodule_helper.rb @@ -0,0 +1,76 @@ +module Gitlab + module SubmoduleHelper + include Gitlab::ShellAdapter + + # links to files listing for submodule if submodule is a project on this server + def submodule_links(submodule_item, ref = nil, repository = @repository) + url = repository.submodule_url_for(ref, submodule_item.path) + + return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ + + namespace = $1 + project = $2 + project.chomp!('.git') + + if self_url?(url, namespace, project) + return namespace_project_path(namespace, project), + namespace_project_tree_path(namespace, project, + submodule_item.id) + elsif relative_self_url?(url) + relative_self_links(url, submodule_item.id) + elsif github_dot_com_url?(url) + standard_links('github.com', namespace, project, submodule_item.id) + elsif gitlab_dot_com_url?(url) + standard_links('gitlab.com', namespace, project, submodule_item.id) + else + return url, nil + end + end + + protected + + def github_dot_com_url?(url) + url =~ /github\.com[\/:][^\/]+\/[^\/]+\Z/ + end + + def gitlab_dot_com_url?(url) + url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/ + end + + def self_url?(url, namespace, project) + return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/', + project, '.git' ].join('') + url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) + end + + def relative_self_url?(url) + # (./)?(../repo.git) || (./)?(../../project/repo.git) ) + url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/ + end + + def standard_links(host, namespace, project, commit) + base = [ 'https://', host, '/', namespace, '/', project ].join('') + [base, [ base, '/tree/', commit ].join('')] + end + + def relative_self_links(url, commit) + # Map relative links to a namespace and project + # For example: + # ../bar.git -> same namespace, repo bar + # ../foo/bar.git -> namespace foo, repo bar + # ../../foo/bar/baz.git -> namespace bar, repo baz + components = url.split('/') + base = components.pop.gsub(/.git$/, '') + namespace = components.pop.gsub(/^\.\.$/, '') + + if namespace.empty? + namespace = @project.namespace.path + end + + [ + namespace_project_path(namespace, base), + namespace_project_tree_path(namespace, base, commit) + ] + end + end +end diff --git a/app/helpers/gitlab/tab_helper.rb b/app/helpers/gitlab/tab_helper.rb new file mode 100644 index 00000000000..01d36ff84fc --- /dev/null +++ b/app/helpers/gitlab/tab_helper.rb @@ -0,0 +1,133 @@ +module Gitlab + module TabHelper + # Navigation link helper + # + # Returns an `li` element with an 'active' class if the supplied + # controller(s) and/or action(s) are currently active. The content of the + # element is the value passed to the block. + # + # options - The options hash used to determine if the element is "active" (default: {}) + # :controller - One or more controller names to check (optional). + # :action - One or more action names to check (optional). + # :path - A shorthand path, such as 'dashboard#index', to check (optional). + # :html_options - Extra options to be passed to the list element (optional). + # block - An optional block that will become the contents of the returned + # `li` element. + # + # When both :controller and :action are specified, BOTH must match in order + # to be marked as active. When only one is given, either can match. + # + # Examples + # + # # Assuming we're on TreeController#show + # + # # Controller matches, but action doesn't + # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" } + # # => '
  • Hello
  • ' + # + # # Controller matches + # nav_link(controller: [:tree, :refs]) { "Hello" } + # # => '
  • Hello
  • ' + # + # # Several paths + # nav_link(path: ['tree#show', 'profile#show']) { "Hello" } + # # => '
  • Hello
  • ' + # + # # Shorthand path + # nav_link(path: 'tree#show') { "Hello" } + # # => '
  • Hello
  • ' + # + # # Supplying custom options for the list element + # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" } + # # => '
  • Hello
  • ' + # + # Returns a list item element String + def nav_link(options = {}, &block) + klass = active_nav_link?(options) ? 'active' : '' + + # Add our custom class into the html_options, which may or may not exist + # and which may or may not already have a :class key + o = options.delete(:html_options) || {} + o[:class] ||= '' + o[:class] += ' ' + klass + o[:class].strip! + + if block_given? + content_tag(:li, capture(&block), o) + else + content_tag(:li, nil, o) + end + end + + def active_nav_link?(options) + if path = options.delete(:path) + unless path.respond_to?(:each) + path = [path] + end + + path.any? do |single_path| + current_path?(single_path) + end + elsif page = options.delete(:page) + unless page.respond_to?(:each) + page = [page] + end + + page.any? do |single_page| + current_page?(single_page) + end + else + c = options.delete(:controller) + a = options.delete(:action) + + if c && a + # When given both options, make sure BOTH are true + current_controller?(*c) && current_action?(*a) + else + # Otherwise check EITHER option + current_controller?(*c) || current_action?(*a) + end + end + end + + def current_path?(path) + c, a, _ = path.split('#') + current_controller?(c) && current_action?(a) + end + + def project_tab_class + return "active" if current_page?(controller: "/projects", action: :edit, id: @project) + + if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name + "active" + end + end + + def branches_tab_class + if current_controller?(:protected_branches) || + current_controller?(:branches) || + current_page?(namespace_project_repository_path(@project.namespace, + @project)) + 'active' + end + end + + # Use nav_tab for save controller/action but different params + def nav_tab(key, value, &block) + o = {} + o[:class] = "" + + if value.nil? + o[:class] << " active" if params[key].blank? + else + o[:class] << " active" if params[key] == value + end + + if block_given? + content_tag(:li, capture(&block), o) + else + content_tag(:li, nil, o) + end + end + end +end diff --git a/app/helpers/gitlab/tags_helper.rb b/app/helpers/gitlab/tags_helper.rb new file mode 100644 index 00000000000..d694b2c90ce --- /dev/null +++ b/app/helpers/gitlab/tags_helper.rb @@ -0,0 +1,16 @@ +module Gitlab + module TagsHelper + def tag_path(tag) + "/tags/#{tag}" + end + + def tag_list(project) + html = '' + project.tag_list.each do |tag| + html << link_to(tag, tag_path(tag)) + end + + html.html_safe + end + end +end diff --git a/app/helpers/gitlab/tree_helper.rb b/app/helpers/gitlab/tree_helper.rb new file mode 100644 index 00000000000..dc48ff0e6e2 --- /dev/null +++ b/app/helpers/gitlab/tree_helper.rb @@ -0,0 +1,89 @@ +module Gitlab + module TreeHelper + # Sorts a repository's tree so that folders are before files and renders + # their corresponding partials + # + def render_tree(tree) + # Render Folders before Files/Submodules + folders, files, submodules = tree.trees, tree.blobs, tree.submodules + + tree = "" + + # Render folders if we have any + tree << render(partial: 'projects/tree/tree_item', collection: folders, + locals: { type: 'folder' }) if folders.present? + + # Render files if we have any + tree << render(partial: 'projects/tree/blob_item', collection: files, + locals: { type: 'file' }) if files.present? + + # Render submodules if we have any + tree << render(partial: 'projects/tree/submodule_item', + collection: submodules) if submodules.present? + + tree.html_safe + end + + def render_readme(readme) + render_markup(readme.name, readme.data) + end + + # Return an image icon depending on the file type and mode + # + # type - String type of the tree item; either 'folder' or 'file' + # mode - File unix mode + # name - File name + def tree_icon(type, mode, name) + icon("#{file_type_icon_class(type, mode, name)} fw") + end + + def tree_hex_class(content) + "file_#{hexdigest(content.name)}" + end + + # Simple shortcut to File.join + def tree_join(*args) + File.join(*args) + end + + def allowed_tree_edit?(project = nil, ref = nil) + project ||= @project + ref ||= @ref + return false unless project.repository.branch_names.include?(ref) + + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) + end + + def tree_breadcrumbs(tree, max_links = 2) + if @path.present? + part_path = "" + parts = @path.split('/') + + yield('..', nil) if parts.count > max_links + + parts.each do |part| + part_path = File.join(part_path, part) unless part_path.empty? + part_path = part if part_path.empty? + + next unless parts.last(2).include?(part) if parts.count > max_links + yield(part, tree_join(@ref, part_path)) + end + end + end + + def up_dir_path + file = File.join(@path, "..") + tree_join(@ref, file) + end + + # returns the relative path of the first subdir that doesn't have only one directory descendant + def flatten_tree(tree) + subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) + if subtree.count == 1 && subtree.first.dir? + return tree_join(tree.name, flatten_tree(subtree.first)) + else + return tree.name + end + end + end +end diff --git a/app/helpers/gitlab/version_check_helper.rb b/app/helpers/gitlab/version_check_helper.rb new file mode 100644 index 00000000000..46a12cc8c60 --- /dev/null +++ b/app/helpers/gitlab/version_check_helper.rb @@ -0,0 +1,9 @@ +module Gitlab + module VersionCheckHelper + def version_status_badge + if Rails.env.production? + image_tag VersionCheck.new.url + end + end + end +end diff --git a/app/helpers/gitlab/visibility_level_helper.rb b/app/helpers/gitlab/visibility_level_helper.rb new file mode 100644 index 00000000000..feba901f7d7 --- /dev/null +++ b/app/helpers/gitlab/visibility_level_helper.rb @@ -0,0 +1,97 @@ +module Gitlab + module VisibilityLevelHelper + def visibility_level_color(level) + case level + when Gitlab::VisibilityLevel::PRIVATE + 'vs-private' + when Gitlab::VisibilityLevel::INTERNAL + 'vs-internal' + when Gitlab::VisibilityLevel::PUBLIC + 'vs-public' + end + end + + # Return the description for the +level+ argument. + # + # +level+ One of the Gitlab::VisibilityLevel constants + # +form_model+ Either a model object (Project, Snippet, etc.) or the name of + # a Project or Snippet class. + def visibility_level_description(level, form_model) + case form_model.is_a?(String) ? form_model : form_model.class.name + when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' + snippet_visibility_level_description(level) + when 'Project' + project_visibility_level_description(level) + end + end + + def project_visibility_level_description(level) + capture_haml do + haml_tag :span do + case level + when Gitlab::VisibilityLevel::PRIVATE + haml_concat "Project access must be granted explicitly for each user." + when Gitlab::VisibilityLevel::INTERNAL + haml_concat "The project can be cloned by" + haml_concat "any logged in user." + when Gitlab::VisibilityLevel::PUBLIC + haml_concat "The project can be cloned" + haml_concat "without any" + haml_concat "authentication." + end + end + end + end + + def snippet_visibility_level_description(level) + capture_haml do + haml_tag :span do + case level + when Gitlab::VisibilityLevel::PRIVATE + haml_concat "The snippet is visible only for me." + when Gitlab::VisibilityLevel::INTERNAL + haml_concat "The snippet is visible for any logged in user." + when Gitlab::VisibilityLevel::PUBLIC + haml_concat "The snippet can be accessed" + haml_concat "without any" + haml_concat "authentication." + end + end + end + end + + def visibility_level_icon(level) + case level + when Gitlab::VisibilityLevel::PRIVATE + private_icon + when Gitlab::VisibilityLevel::INTERNAL + internal_icon + when Gitlab::VisibilityLevel::PUBLIC + public_icon + end + end + + def visibility_level_label(level) + Project.visibility_levels.key(level) + end + + def restricted_visibility_levels(show_all = false) + return [] if current_user.is_admin? && !show_all + current_application_settings.restricted_visibility_levels || [] + end + + def default_project_visibility + current_application_settings.default_project_visibility + end + + def default_snippet_visibility + current_application_settings.default_snippet_visibility + end + + def skip_level?(form_model, level) + form_model.is_a?(Project) && + form_model.forked? && + !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level) + end + end +end diff --git a/app/helpers/gitlab/wiki_helper.rb b/app/helpers/gitlab/wiki_helper.rb new file mode 100644 index 00000000000..02a1daf0019 --- /dev/null +++ b/app/helpers/gitlab/wiki_helper.rb @@ -0,0 +1,26 @@ +module Gitlab + module WikiHelper + # Rails v4.1.9+ escapes all model IDs, converting slashes into %2F. The + # only way around this is to implement our own path generators. + def namespace_project_wiki_path(namespace, project, wiki_page, *args) + slug = + case wiki_page + when Symbol + wiki_page + when String + wiki_page + else + wiki_page.slug + end + namespace_project_path(namespace, project) + "/wikis/#{slug}" + end + + def edit_namespace_project_wiki_path(namespace, project, wiki_page, *args) + namespace_project_wiki_path(namespace, project, wiki_page) + '/edit' + end + + def history_namespace_project_wiki_path(namespace, project, wiki_page, *args) + namespace_project_wiki_path(namespace, project, wiki_page) + '/history' + end + end +end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb deleted file mode 100644 index eb3f72a307d..00000000000 --- a/app/helpers/gitlab_markdown_helper.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'nokogiri' - -module GitlabMarkdownHelper - include Gitlab::Markdown - include PreferencesHelper - - # Use this in places where you would normally use link_to(gfm(...), ...). - # - # It solves a problem occurring with nested links (i.e. - # "outer text gfm ref more outer text"). This will not be - # interpreted as intended. Browsers will parse something like - # "outer text gfm ref more outer text" (notice the last part is - # not linked any more). link_to_gfm corrects that. It wraps all parts to - # explicitly produce the correct linking behavior (i.e. - # "outer text gfm ref more outer text"). - def link_to_gfm(body, url, html_options = {}) - return "" if body.blank? - - escaped_body = if body =~ /\A\ at the beginning of a line", - "Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___" - ].freeze - - # Returns a random markdown tip for use as a textarea placeholder - def random_markdown_tip - MARKDOWN_TIPS.sample - end - - private - - # Return +text+, truncated to +max_chars+ characters, excluding any HTML - # tags. - def truncate_visible(text, max_chars) - doc = Nokogiri::HTML.fragment(text) - content_length = 0 - truncated = false - - doc.traverse do |node| - if node.text? || node.content.empty? - if truncated - node.remove - next - end - - # Handle line breaks within a node - if node.content.strip.lines.length > 1 - node.content = "#{node.content.lines.first.chomp}..." - truncated = true - end - - num_remaining = max_chars - content_length - if node.content.length > num_remaining - node.content = node.content.truncate(num_remaining) - truncated = true - end - content_length += node.content.length - end - - truncated = truncate_if_block(node, truncated) - end - - doc.to_html - end - - # Used by #truncate_visible. If +node+ is the first block element, and the - # text hasn't already been truncated, then append "..." to the node contents - # and return true. Otherwise return false. - def truncate_if_block(node, truncated) - if node.element? && node.description.block? && !truncated - node.content = "#{node.content}..." if node.next_sibling - true - else - truncated - end - end - - # Returns the text necessary to reference `entity` across projects - # - # project - Project to reference - # entity - Object that responds to `to_reference` - # - # Examples: - # - # cross_project_reference(project, project.issues.first) - # # => 'namespace1/project1#123' - # - # cross_project_reference(project, project.merge_requests.first) - # # => 'namespace1/project1!345' - # - # Returns a String - def cross_project_reference(project, entity) - if entity.respond_to?(:to_reference) - "#{project.to_reference}#{entity.to_reference}" - else - '' - end - end -end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb deleted file mode 100644 index d0fae255a04..00000000000 --- a/app/helpers/gitlab_routing_helper.rb +++ /dev/null @@ -1,67 +0,0 @@ -# Shorter routing method for project and project items -# Since update to rails 4.1.9 we are now allowed to use `/` in project routing -# so we use nested routing for project resources which include project and -# project namespace. To avoid writing long methods every time we define shortcuts for -# some of routing. -# -# For example instead of this: -# -# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request) -# -# We can simply use shortcut: -# -# merge_request_path(merge_request) -# -module GitlabRoutingHelper - def project_path(project, *args) - namespace_project_path(project.namespace, project, *args) - end - - def activity_project_path(project, *args) - 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 issue_path(entity, *args) - namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) - end - - def merge_request_path(entity, *args) - namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) - end - - def milestone_path(entity, *args) - 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 - - def merge_request_url(entity, *args) - namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) - end - - def project_snippet_url(entity, *args) - namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) - end - - def toggle_subscription_path(entity, *args) - if entity.is_a?(Issue) - toggle_subscription_namespace_project_issue_path(entity.project.namespace, entity.project, entity) - else - toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity) - end - end -end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb deleted file mode 100644 index e1dda20de85..00000000000 --- a/app/helpers/graph_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -module GraphHelper - def get_refs(repo, commit) - refs = "" - refs << commit.ref_names(repo).join(' ') - - # append note count - refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 - - refs - end - - def parents_zip_spaces(parents, parent_spaces) - ids = parents.map { |p| p.id } - ids.zip(parent_spaces) - end -end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb deleted file mode 100644 index b067cb54a43..00000000000 --- a/app/helpers/groups_helper.rb +++ /dev/null @@ -1,33 +0,0 @@ -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 group_icon(group) - if group.is_a?(String) - group = Group.find_by(path: group) - end - - if group && group.avatar.present? - group.avatar.url - else - image_path('no_group_avatar.png') - end - end -end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb deleted file mode 100644 index 1cf5b96481a..00000000000 --- a/app/helpers/icons_helper.rb +++ /dev/null @@ -1,85 +0,0 @@ -module IconsHelper - include FontAwesome::Rails::IconHelper - - # Creates an icon tag given icon name(s) and possible icon modifiers. - # - # Right now this method simply delegates directly to `fa_icon` from the - # font-awesome-rails gem, but should we ever use a different icon pack in the - # future we won't have to change hundreds of method calls. - def icon(names, options = {}) - fa_icon(names, options) - end - - def spinner(text = nil, visible = false) - css_class = 'loading' - css_class << ' hide' unless visible - - content_tag :div, class: css_class do - icon('spinner spin') + text - end - end - - def boolean_to_icon(value) - if value - icon('circle', class: 'cgreen') - else - icon('power-off', class: 'clgray') - end - end - - def public_icon - icon('globe fw') - end - - def internal_icon - icon('shield fw') - end - - def private_icon - icon('lock fw') - end - - def file_type_icon_class(type, mode, name) - if type == 'folder' - icon_class = 'folder' - elsif mode == '120000' - icon_class = 'share' - else - # Guess which icon to choose based on file extension. - # If you think a file extension is missing, feel free to add it on PR - - case File.extname(name).downcase - when '.pdf' - icon_class = 'file-pdf-o' - when '.jpg', '.jpeg', '.jif', '.jfif', - '.jp2', '.jpx', '.j2k', '.j2c', - '.png', '.gif', '.tif', '.tiff', - '.svg', '.ico', '.bmp' - icon_class = 'file-image-o' - when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip', - '.xz', '.rar', '.7z' - icon_class = 'file-archive-o' - when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac' - icon_class = 'file-audio-o' - when '.mp4', '.m4p', '.m4v', - '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv', - '.mpg', '.mpeg', '.m2v', - '.avi', '.mkv', '.flv', '.ogv', '.mov', - '.3gp', '.3g2' - icon_class = 'file-video-o' - when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb' - icon_class = 'file-word-o' - when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm', - '.xlsb', '.xla', '.xlam', '.xll', '.xlw' - icon_class = 'file-excel-o' - when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm', - '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm' - icon_class = 'file-powerpoint-o' - else - icon_class = 'file-text-o' - end - end - - icon_class - end -end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb deleted file mode 100644 index 6ddb37cd0dc..00000000000 --- a/app/helpers/issues_helper.rb +++ /dev/null @@ -1,88 +0,0 @@ -module IssuesHelper - def issue_css_classes(issue) - classes = "issue" - classes << " closed" if issue.closed? - classes << " today" if issue.today? - classes - end - - # Returns an OpenStruct object suitable for use by options_from_collection_for_select - # to allow filtering issues by an unassigned User or Milestone - def unassigned_filter - # Milestone uses :title, Issue uses :name - OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') - end - - def url_for_project_issues(project = @project, options = {}) - return '' if project.nil? - - if options[:only_path] - project.issues_tracker.project_path - else - project.issues_tracker.project_url - end - end - - def url_for_new_issue(project = @project, options = {}) - return '' if project.nil? - - if options[:only_path] - project.issues_tracker.new_issue_path - else - project.issues_tracker.new_issue_url - end - end - - def url_for_issue(issue_iid, project = @project, options = {}) - return '' if project.nil? - - if options[:only_path] - project.issues_tracker.issue_path(issue_iid) - else - project.issues_tracker.issue_url(issue_iid) - end - end - - def bulk_update_milestone_options - options_for_select([['None (backlog)', -1]]) + - options_from_collection_for_select(project_active_milestones, 'id', - 'title', params[:milestone_id]) - end - - def milestone_options(object) - options_from_collection_for_select(object.project.milestones.active, - 'id', 'title', object.milestone_id) - end - - def issue_box_class(item) - if item.respond_to?(:expired?) && item.expired? - 'issue-box-expired' - elsif item.respond_to?(:merged?) && item.merged? - 'issue-box-merged' - elsif item.closed? - 'issue-box-closed' - else - 'issue-box-open' - end - end - - def issue_to_atom(xml, issue) - xml.entry do - xml.id namespace_project_issue_url(issue.project.namespace, - issue.project, issue) - xml.link href: namespace_project_issue_url(issue.project.namespace, - issue.project, issue) - xml.title truncate(issue.title, length: 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") - xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email) - xml.author do |author| - xml.name issue.author_name - xml.email issue.author_email - end - xml.summary issue.title - end - end - - # Required for Gitlab::Markdown::IssueReferenceFilter - module_function :url_for_issue -end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb deleted file mode 100644 index 8036303851b..00000000000 --- a/app/helpers/labels_helper.rb +++ /dev/null @@ -1,101 +0,0 @@ -module LabelsHelper - include ActionView::Helpers::TagHelper - - # Link to a Label - # - # label - Label object to link to - # project - Project object which will be used as the context for the label's - # link. If omitted, defaults to `@project`, or the label's own - # project. - # block - An optional block that will be passed to `link_to`, forming the - # body of the link element. If omitted, defaults to - # `render_colored_label`. - # - # Examples: - # - # # Allow the generated link to use the label's own project - # link_to_label(label) - # - # # Force the generated link to use @project - # @project = Project.first - # link_to_label(label) - # - # # Force the generated link to use a provided project - # link_to_label(label, project: Project.last) - # - # # Customize link body with a block - # link_to_label(label) { "My Custom Label Text" } - # - # Returns a String - def link_to_label(label, project: nil, &block) - project ||= @project || label.project - link = namespace_project_issues_path(project.namespace, project, - label_name: label.name) - - if block_given? - link_to link, &block - else - link_to render_colored_label(label), link - end - end - - def project_label_names - @project.labels.pluck(:title) - end - - def render_colored_label(label) - label_color = label.color || Label::DEFAULT_COLOR - text_color = text_color_for_bg(label_color) - - # Intentionally not using content_tag here so that this method can be called - # by LabelReferenceFilter - span = %() + - escape_once(label.name) + '' - - span.html_safe - end - - def suggested_colors - [ - '#0033CC', - '#428BCA', - '#44AD8E', - '#A8D695', - '#5CB85C', - '#69D100', - '#004E00', - '#34495E', - '#7F8C8D', - '#A295D6', - '#5843AD', - '#8E44AD', - '#FFECDB', - '#AD4363', - '#D10069', - '#CC0033', - '#FF0000', - '#D9534F', - '#D1D100', - '#F0AD4E', - '#AD8D43' - ] - end - - def text_color_for_bg(bg_color) - r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex) - - if (r + g + b) > 500 - '#333333' - else - '#FFFFFF' - end - end - - def project_labels_options(project) - options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) - end - - # Required for Gitlab::Markdown::LabelReferenceFilter - module_function :render_colored_label, :text_color_for_bg, :escape_once -end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb deleted file mode 100644 index f8169b4f288..00000000000 --- a/app/helpers/merge_requests_helper.rb +++ /dev/null @@ -1,74 +0,0 @@ -module MergeRequestsHelper - def new_mr_path_from_push_event(event) - target_project = event.project.forked_from_project || event.project - new_namespace_project_merge_request_path( - event.project.namespace, - event.project, - new_mr_from_push_event(event, target_project) - ) - end - - def new_mr_path_for_fork_from_push_event(event) - new_namespace_project_merge_request_path( - event.project.namespace, - event.project, - new_mr_from_push_event(event, event.project.forked_from_project) - ) - end - - def new_mr_from_push_event(event, target_project) - { - merge_request: { - source_project_id: event.project.id, - target_project_id: target_project.id, - source_branch: event.branch_name, - target_branch: target_project.repository.root_ref - } - } - end - - def mr_css_classes(mr) - classes = "merge-request" - classes << " closed" if mr.closed? - classes << " merged" if mr.merged? - classes - end - - def ci_build_details_path(merge_request) - merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) - end - - def merge_path_description(merge_request, separator) - if merge_request.for_fork? - "Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.path_with_namespace}:#{@merge_request.target_branch}" - else - "Branches: #{@merge_request.source_branch} #{separator} #{@merge_request.target_branch}" - end - end - - def issues_sentence(issues) - issues.map { |i| "##{i.iid}" }.to_sentence - end - - def mr_change_branches_path(merge_request) - new_namespace_project_merge_request_path( - @project.namespace, @project, - merge_request: { - source_project_id: @merge_request.source_project_id, - target_project_id: @merge_request.target_project_id, - source_branch: @merge_request.source_branch, - target_branch: nil - } - ) - end - - def source_branch_with_namespace(merge_request) - if merge_request.for_fork? - namespace = link_to(merge_request.source_project_namespace, - project_path(merge_request.source_project)) - namespace + ":#{merge_request.source_branch}" - else - merge_request.source_branch - end - end -end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb deleted file mode 100644 index 132a893e532..00000000000 --- a/app/helpers/milestones_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -module MilestonesHelper - def milestones_filter_path(opts = {}) - if @project - namespace_project_milestones_path(@project.namespace, @project, opts) - elsif @group - group_milestones_path(@group, opts) - else - dashboard_milestones_path(opts) - end - end - - def milestone_progress_bar(milestone) - options = { - class: 'progress-bar progress-bar-success', - style: "width: #{milestone.percent_complete}%;" - } - - content_tag :div, class: 'progress' do - content_tag :div, nil, options - end - end - - def projects_milestones_options - milestones = - if @project - @project.milestones - else - Milestone.where(project_id: @projects) - end.active - - grouped_milestones = Milestones::GroupService.new(milestones).execute - grouped_milestones.unshift(Milestone::None) - - options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title]) - end -end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb deleted file mode 100644 index b3132a1f3ba..00000000000 --- a/app/helpers/namespaces_helper.rb +++ /dev/null @@ -1,36 +0,0 @@ -module NamespacesHelper - def namespaces_options(selected = :current_user, scope = :default) - groups = current_user.owned_groups + current_user.masters_groups - users = [current_user.namespace] - - group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ] - users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ] - - options = [] - options << group_opts - options << users_opts - - if selected == :current_user && current_user.namespace - selected = current_user.namespace.id - end - - grouped_options_for_select(options, selected) - end - - def namespace_select_tag(id, opts = {}) - css_class = "ajax-namespace-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - - hidden_field_tag(id, value, class: css_class) - end - - def namespace_icon(namespace, size = 40) - if namespace.kind_of?(Group) - group_icon(namespace) - else - avatar_icon(namespace.owner.email, size) - end - end -end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb deleted file mode 100644 index 9b1dd8b8e54..00000000000 --- a/app/helpers/nav_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -module NavHelper - def nav_menu_collapsed? - cookies[:collapsed_nav] == 'true' - end - - def nav_sidebar_class - if nav_menu_collapsed? - "page-sidebar-collapsed" - else - "page-sidebar-expanded" - end - end - - def nav_header_class - if nav_menu_collapsed? - "header-collapsed" - else - "header-expanded" - end - end -end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb deleted file mode 100644 index 5f0c921413a..00000000000 --- a/app/helpers/notes_helper.rb +++ /dev/null @@ -1,76 +0,0 @@ -module NotesHelper - # Helps to distinguish e.g. commit notes in mr notes list - def note_for_main_target?(note) - (@noteable.class.name == note.noteable_type && !note.for_diff_line?) - end - - def note_target_fields(note) - hidden_field_tag(:target_type, note.noteable.class.name.underscore) + - hidden_field_tag(:target_id, note.noteable.id) - end - - def note_editable?(note) - note.editable? && can?(current_user, :admin_note, note) - end - - def link_to_commit_diff_line_note(note) - if note.for_commit_diff_line? - link_to( - "#{note.diff_file_name}:L#{note.diff_new_line}", - namespace_project_commit_path(@project.namespace, @project, - note.noteable, anchor: note.line_code) - ) - end - end - - def noteable_json(noteable) - { - id: noteable.id, - class: noteable.class.name, - resources: noteable.class.table_name, - project_id: noteable.project.id, - }.to_json - end - - def link_to_new_diff_note(line_code, line_type = nil) - discussion_id = Note.build_discussion_id( - @comments_target[:noteable_type], - @comments_target[:noteable_id] || @comments_target[:commit_id], - line_code - ) - - data = { - noteable_type: @comments_target[:noteable_type], - noteable_id: @comments_target[:noteable_id], - commit_id: @comments_target[:commit_id], - line_code: line_code, - discussion_id: discussion_id, - line_type: line_type - } - - button_tag(class: 'btn add-diff-note js-add-diff-note-button', - data: data, - title: 'Add a comment to this line') do - icon('comment-o') - end - end - - def link_to_reply_diff(note, line_type = nil) - return unless current_user - - data = { - noteable_type: note.noteable_type, - noteable_id: note.noteable_id, - commit_id: note.commit_id, - line_code: note.line_code, - discussion_id: note.discussion_id, - line_type: line_type - } - - button_tag class: 'btn reply-btn js-discussion-reply-button', - data: data, title: 'Add a reply' do - link_text = icon('comment') - link_text << ' Reply' - end - end -end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb deleted file mode 100644 index 2f8e64c375f..00000000000 --- a/app/helpers/notifications_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -module NotificationsHelper - include IconsHelper - - def notification_icon(notification) - if notification.disabled? - icon('volume-off', class: 'ns-mute') - elsif notification.participating? - icon('volume-down', class: 'ns-part') - elsif notification.watch? - icon('volume-up', class: 'ns-watch') - else - icon('circle-o', class: 'ns-default') - end - end -end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb deleted file mode 100644 index 01b6a63552c..00000000000 --- a/app/helpers/page_layout_helper.rb +++ /dev/null @@ -1,26 +0,0 @@ -module PageLayoutHelper - def page_title(*titles) - @page_title ||= [] - - @page_title.push(*titles.compact) if titles.any? - - @page_title.join(" | ") - end - - def header_title(title = nil, title_url = nil) - if title - @header_title = title - @header_title_url = title_url - else - @header_title_url ? link_to(@header_title, @header_title_url) : @header_title - end - end - - def sidebar(name = nil) - if name - @sidebar = name - else - @sidebar - end - end -end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb deleted file mode 100644 index ea774e28ecf..00000000000 --- a/app/helpers/preferences_helper.rb +++ /dev/null @@ -1,65 +0,0 @@ -# Helper methods for per-User preferences -module PreferencesHelper - COLOR_SCHEMES = { - 1 => 'white', - 2 => 'dark', - 3 => 'solarized-light', - 4 => 'solarized-dark', - 5 => 'monokai', - } - COLOR_SCHEMES.default = 'white' - - # Helper method to access the COLOR_SCHEMES - # - # The keys are the `color_scheme_ids` - # The values are the `name` of the scheme. - # - # The preview images are `name-scheme-preview.png` - # The stylesheets should use the css class `.name` - def color_schemes - COLOR_SCHEMES.freeze - end - - # Maps `dashboard` values to more user-friendly option text - DASHBOARD_CHOICES = { - projects: 'Your Projects (default)', - stars: 'Starred Projects' - }.with_indifferent_access.freeze - - # Returns an Array usable by a select field for more user-friendly option text - def dashboard_choices - defined = User.dashboards - - if defined.size != DASHBOARD_CHOICES.size - # Ensure that anyone adding new options updates this method too - raise RuntimeError, "`User` defines #{defined.size} dashboard choices," + - " but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}." - else - defined.map do |key, _| - # Use `fetch` so `KeyError` gets raised when a key is missing - [DASHBOARD_CHOICES.fetch(key), key] - end - end - end - - def project_view_choices - [ - ['Readme (default)', :readme], - ['Activity view', :activity] - ] - end - - def user_application_theme - theme = Gitlab::Themes.by_id(current_user.try(:theme_id)) - theme.css_class - end - - def user_color_scheme_class - COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) - end - - def prefer_readme? - !current_user || - current_user.project_view == 'readme' - end -end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb deleted file mode 100644 index ab9b068de05..00000000000 --- a/app/helpers/projects_helper.rb +++ /dev/null @@ -1,330 +0,0 @@ -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] do - title = content_tag(:span, project.name, class: 'project-name') - - if project.namespace - namespace = content_tag(:span, "#{project.namespace.human_name} / ", class: 'namespace-name') - title = namespace + title - end - - title - end - end - - def link_to_member(project, author, opts = {}) - default_opts = { avatar: true, name: true, size: 16, author_class: 'author' } - opts = default_opts.merge(opts) - - return "(deleted)" unless author - - author_html = "" - - # Build avatar image tag - author_html << image_tag(avatar_icon(author.try(:email), opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] - - # Build name span tag - author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] - - author_html = author_html.html_safe - - if opts[:name] - link_to(author_html, user_path(author), class: "author_link").html_safe - else - link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe - end - end - - def project_title(project) - if project.group - content_tag :span do - link_to( - simple_sanitize(project.group.name), group_path(project.group) - ) + ' / ' + - link_to(simple_sanitize(project.name), - project_path(project)) - end - else - owner = project.namespace.owner - content_tag :span do - link_to( - simple_sanitize(owner.name), user_path(owner) - ) + ' / ' + - link_to(simple_sanitize(project.name), - project_path(project)) - end - end - end - - def remove_project_message(project) - "You are going to remove #{project.name_with_namespace}.\n Removed project CANNOT be restored!\n Are you ABSOLUTELY sure?" - end - - def transfer_project_message(project) - "You are going to transfer #{project.name_with_namespace} to another owner. Are you ABSOLUTELY sure?" - end - - def project_nav_tabs - @nav_tabs ||= get_project_nav_tabs(@project, current_user) - end - - def project_nav_tab?(name) - project_nav_tabs.include? name - end - - def project_active_milestones - @project.milestones.active.order("due_date, title ASC") - end - - def project_for_deploy_key(deploy_key) - if deploy_key.projects.include?(@project) - @project - else - deploy_key.projects.find { |project| can?(current_user, :read_project, project) } - end - end - - def can_change_visibility_level?(project, current_user) - return false unless can?(current_user, :change_visibility_level, project) - - if project.forked? - project.forked_from_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE - else - true - end - end - - private - - def get_project_nav_tabs(project, current_user) - nav_tabs = [:home] - - if !project.empty_repo? && can?(current_user, :download_code, project) - nav_tabs << [:files, :commits, :network, :graphs] - end - - if project.repo_exists? && can?(current_user, :read_merge_request, project) - nav_tabs << :merge_requests - end - - if can?(current_user, :admin_project, project) - nav_tabs << :settings - end - - if can?(current_user, :read_issue, project) - nav_tabs << :issues - end - - if can?(current_user, :read_wiki, project) - nav_tabs << :wiki - end - - if can?(current_user, :read_project_snippet, project) - nav_tabs << :snippets - end - - if can?(current_user, :read_label, project) - nav_tabs << :labels - end - - if can?(current_user, :read_milestone, project) - nav_tabs << :milestones - end - - nav_tabs.flatten - end - - def git_user_name - if current_user - current_user.name - else - "Your name" - end - end - - def git_user_email - if current_user - current_user.email - else - "your@email.com" - end - end - - def repository_size(project = nil) - "#{(project || @project).repository_size} MB" - rescue - # In order to prevent 500 error - # when application cannot allocate memory - # to calculate repo size - just show 'Unknown' - 'unknown' - end - - def default_url_to_repo(project = nil) - project = project || @project - current_user ? project.url_to_repo : project.http_url_to_repo - end - - def default_clone_protocol - current_user ? "ssh" : "http" - end - - def project_last_activity(project) - if project.last_activity_at - time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago') - else - "Never" - end - end - - def add_contribution_guide_path(project) - if project && !project.repository.contribution_guide - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CONTRIBUTING.md", - commit_message: "Add contribution guide" - ) - end - end - - def add_changelog_path(project) - if project && !project.repository.changelog - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "CHANGELOG", - commit_message: "Add changelog" - ) - end - end - - def add_license_path(project) - if project && !project.repository.license - namespace_project_new_blob_path( - project.namespace, - project, - project.default_branch, - file_name: "LICENSE", - commit_message: "Add license" - ) - end - end - - def contribution_guide_path(project) - if project && contribution_guide = project.repository.contribution_guide - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - contribution_guide.name) - ) - end - end - - def readme_path(project) - filename_path(project, :readme) - end - - def changelog_path(project) - filename_path(project, :changelog) - end - - def license_path(project) - filename_path(project, :license) - end - - def version_path(project) - filename_path(project, :version) - end - - def hidden_pass_url(original_url) - result = URI(original_url) - result.password = '*****' unless result.password.nil? - result - rescue - original_url - end - - def project_wiki_path_with_version(proj, page, version, is_newest) - url_params = is_newest ? {} : { version_id: version } - namespace_project_wiki_path(proj.namespace, proj, page, url_params) - end - - def project_status_css_class(status) - case status - when "started" - "active" - when "failed" - "danger" - when "finished" - "success" - end - end - - def user_max_access_in_project(user, project) - level = project.team.max_member_access(user) - - if level - Gitlab::Access.options_with_owner.key(level) - 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' - - namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') - end - - def last_push_event - if current_user - current_user.recent_push(@project.id) - end - end - - def readme_cache_key - sha = @project.commit.try(:sha) || 'nil' - [@project.id, sha, "readme"].join('-') - end - - def round_commit_count(project) - count = project.commit_count - - if count > 10000 - '10000+' - elsif count > 5000 - '5000+' - elsif count > 1000 - '1000+' - else - count - end - end - - private - - def filename_path(project, filename) - if project && blob = project.repository.send(filename) - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - blob.name) - ) - end - end -end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb deleted file mode 100644 index c31a556ff7b..00000000000 --- a/app/helpers/search_helper.rb +++ /dev/null @@ -1,112 +0,0 @@ -module SearchHelper - def search_autocomplete_opts(term) - return unless current_user - - resources_results = [ - groups_autocomplete(term), - projects_autocomplete(term) - ].flatten - - generic_results = project_autocomplete + default_autocomplete + help_autocomplete - generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") } - - [ - resources_results, - generic_results - ].flatten.uniq do |item| - item[:label] - end - end - - private - - # Autocomplete results for various settings pages - def default_autocomplete - [ - { label: "Profile settings", url: profile_path }, - { label: "SSH Keys", url: profile_keys_path }, - { label: "Dashboard", url: root_path }, - { label: "Admin Section", url: admin_root_path }, - ] - end - - # Autocomplete results for internal help pages - def help_autocomplete - [ - { label: "help: API Help", url: help_page_path("api", "README") }, - { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, - { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, - { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, - { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, - { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, - ] - end - - # Autocomplete results for the current project, if it's defined - def project_autocomplete - if @project && @project.repository.exists? && @project.repository.root_ref - prefix = search_result_sanitize(@project.name_with_namespace) - ref = @ref || @project.repository.root_ref - - [ - { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, - ] - else - [] - end - end - - # Autocomplete results for the current user's groups - def groups_autocomplete(term, limit = 5) - current_user.authorized_groups.search(term).limit(limit).map do |group| - { - label: "group: #{search_result_sanitize(group.name)}", - url: group_path(group) - } - end - end - - # Autocomplete results for the current user's projects - def projects_autocomplete(term, limit = 5) - ProjectsFinder.new.execute(current_user).search_by_title(term). - sorted_by_stars.non_archived.limit(limit).map do |p| - { - label: "project: #{search_result_sanitize(p.name_with_namespace)}", - url: namespace_project_path(p.namespace, p) - } - end - end - - def search_result_sanitize(str) - Sanitize.clean(str) - end - - def search_filter_path(options={}) - exist_opts = { - search: params[:search], - project_id: params[:project_id], - group_id: params[:group_id], - scope: params[:scope] - } - - options = exist_opts.merge(options) - search_path(options) - end - - # Sanitize html generated after parsing markdown from issue description or comment - def search_md_sanitize(html) - sanitize(html, tags: %w(a p ol ul li pre code)) - end -end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb deleted file mode 100644 index 12fce8db701..00000000000 --- a/app/helpers/selects_helper.rb +++ /dev/null @@ -1,45 +0,0 @@ -module SelectsHelper - def users_select_tag(id, opts = {}) - css_class = "ajax-users-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - placeholder = opts[:placeholder] || 'Search for a user' - - null_user = opts[:null_user] || false - any_user = opts[:any_user] || false - email_user = opts[:email_user] || false - first_user = opts[:first_user] && current_user ? current_user.username : false - current_user = opts[:current_user] || false - project = opts[:project] || @project - - html = { - class: css_class, - 'data-placeholder' => placeholder, - 'data-null-user' => null_user, - 'data-any-user' => any_user, - 'data-email-user' => email_user, - 'data-first-user' => first_user, - 'data-current-user' => current_user - } - - unless opts[:scope] == :all - if project - html['data-project-id'] = project.id - elsif @group - html['data-group-id'] = @group.id - end - end - - hidden_field_tag(id, value, html) - end - - def groups_select_tag(id, opts = {}) - css_class = "ajax-groups-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - - hidden_field_tag(id, value, class: css_class) - end -end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb deleted file mode 100644 index 906cb12cd48..00000000000 --- a/app/helpers/snippets_helper.rb +++ /dev/null @@ -1,20 +0,0 @@ -module SnippetsHelper - def lifetime_select_options - options = [ - ['forever', nil], - ['1 day', "#{Date.current + 1.day}"], - ['1 week', "#{Date.current + 1.week}"], - ['1 month', "#{Date.current + 1.month}"] - ] - options_for_select(options) - end - - def reliable_snippet_path(snippet) - if snippet.project_id? - namespace_project_snippet_path(snippet.project.namespace, - snippet.project, snippet) - else - snippet_path(snippet) - end - end -end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb deleted file mode 100644 index bb12d43f397..00000000000 --- a/app/helpers/sorting_helper.rb +++ /dev/null @@ -1,96 +0,0 @@ -module SortingHelper - def sort_options_hash - { - sort_value_name => sort_title_name, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated, - sort_value_recently_created => sort_title_recently_created, - sort_value_oldest_created => sort_title_oldest_created, - sort_value_milestone_soon => sort_title_milestone_soon, - sort_value_milestone_later => sort_title_milestone_later, - sort_value_largest_repo => sort_title_largest_repo, - sort_value_recently_signin => sort_title_recently_signin, - sort_value_oldest_signin => sort_title_oldest_signin, - } - end - - def sort_title_oldest_updated - 'Oldest updated' - end - - def sort_title_recently_updated - 'Recently updated' - end - - def sort_title_oldest_created - 'Oldest created' - end - - def sort_title_recently_created - 'Recently created' - end - - def sort_title_milestone_soon - 'Milestone due soon' - end - - def sort_title_milestone_later - 'Milestone due later' - end - - def sort_title_name - 'Name' - end - - def sort_title_largest_repo - 'Largest repository' - end - - def sort_title_recently_signin - 'Recent sign in' - end - - def sort_title_oldest_signin - 'Oldest sign in' - end - - def sort_value_oldest_updated - 'updated_asc' - end - - def sort_value_recently_updated - 'updated_desc' - end - - def sort_value_oldest_created - 'created_asc' - end - - def sort_value_recently_created - 'created_desc' - end - - def sort_value_milestone_soon - 'milestone_due_asc' - end - - def sort_value_milestone_later - 'milestone_due_desc' - end - - def sort_value_name - 'name_asc' - end - - def sort_value_largest_repo - 'repository_size_desc' - end - - def sort_value_recently_signin - 'recent_sign_in' - end - - def sort_value_oldest_signin - 'oldest_sign_in' - end -end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb deleted file mode 100644 index b3f50ceebe4..00000000000 --- a/app/helpers/submodule_helper.rb +++ /dev/null @@ -1,74 +0,0 @@ -module SubmoduleHelper - include Gitlab::ShellAdapter - - # links to files listing for submodule if submodule is a project on this server - def submodule_links(submodule_item, ref = nil, repository = @repository) - url = repository.submodule_url_for(ref, submodule_item.path) - - return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ - - namespace = $1 - project = $2 - project.chomp!('.git') - - if self_url?(url, namespace, project) - return namespace_project_path(namespace, project), - namespace_project_tree_path(namespace, project, - submodule_item.id) - elsif relative_self_url?(url) - relative_self_links(url, submodule_item.id) - elsif github_dot_com_url?(url) - standard_links('github.com', namespace, project, submodule_item.id) - elsif gitlab_dot_com_url?(url) - standard_links('gitlab.com', namespace, project, submodule_item.id) - else - return url, nil - end - end - - protected - - def github_dot_com_url?(url) - url =~ /github\.com[\/:][^\/]+\/[^\/]+\Z/ - end - - def gitlab_dot_com_url?(url) - url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/ - end - - def self_url?(url, namespace, project) - return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/', - project, '.git' ].join('') - url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) - end - - def relative_self_url?(url) - # (./)?(../repo.git) || (./)?(../../project/repo.git) ) - url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/ - end - - def standard_links(host, namespace, project, commit) - base = [ 'https://', host, '/', namespace, '/', project ].join('') - [base, [ base, '/tree/', commit ].join('')] - end - - def relative_self_links(url, commit) - # Map relative links to a namespace and project - # For example: - # ../bar.git -> same namespace, repo bar - # ../foo/bar.git -> namespace foo, repo bar - # ../../foo/bar/baz.git -> namespace bar, repo baz - components = url.split('/') - base = components.pop.gsub(/.git$/, '') - namespace = components.pop.gsub(/^\.\.$/, '') - - if namespace.empty? - namespace = @project.namespace.path - end - - [ - namespace_project_path(namespace, base), - namespace_project_tree_path(namespace, base, commit) - ] - end -end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb deleted file mode 100644 index 0e7d8065ac7..00000000000 --- a/app/helpers/tab_helper.rb +++ /dev/null @@ -1,131 +0,0 @@ -module TabHelper - # Navigation link helper - # - # Returns an `li` element with an 'active' class if the supplied - # controller(s) and/or action(s) are currently active. The content of the - # element is the value passed to the block. - # - # options - The options hash used to determine if the element is "active" (default: {}) - # :controller - One or more controller names to check (optional). - # :action - One or more action names to check (optional). - # :path - A shorthand path, such as 'dashboard#index', to check (optional). - # :html_options - Extra options to be passed to the list element (optional). - # block - An optional block that will become the contents of the returned - # `li` element. - # - # When both :controller and :action are specified, BOTH must match in order - # to be marked as active. When only one is given, either can match. - # - # Examples - # - # # Assuming we're on TreeController#show - # - # # Controller matches, but action doesn't - # nav_link(controller: [:tree, :refs], action: :edit) { "Hello" } - # # => '
  • Hello
  • ' - # - # # Controller matches - # nav_link(controller: [:tree, :refs]) { "Hello" } - # # => '
  • Hello
  • ' - # - # # Several paths - # nav_link(path: ['tree#show', 'profile#show']) { "Hello" } - # # => '
  • Hello
  • ' - # - # # Shorthand path - # nav_link(path: 'tree#show') { "Hello" } - # # => '
  • Hello
  • ' - # - # # Supplying custom options for the list element - # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" } - # # => '
  • Hello
  • ' - # - # Returns a list item element String - def nav_link(options = {}, &block) - klass = active_nav_link?(options) ? 'active' : '' - - # Add our custom class into the html_options, which may or may not exist - # and which may or may not already have a :class key - o = options.delete(:html_options) || {} - o[:class] ||= '' - o[:class] += ' ' + klass - o[:class].strip! - - if block_given? - content_tag(:li, capture(&block), o) - else - content_tag(:li, nil, o) - end - end - - def active_nav_link?(options) - if path = options.delete(:path) - unless path.respond_to?(:each) - path = [path] - end - - path.any? do |single_path| - current_path?(single_path) - end - elsif page = options.delete(:page) - unless page.respond_to?(:each) - page = [page] - end - - page.any? do |single_page| - current_page?(single_page) - end - else - c = options.delete(:controller) - a = options.delete(:action) - - if c && a - # When given both options, make sure BOTH are true - current_controller?(*c) && current_action?(*a) - else - # Otherwise check EITHER option - current_controller?(*c) || current_action?(*a) - end - end - end - - def current_path?(path) - c, a, _ = path.split('#') - current_controller?(c) && current_action?(a) - end - - def project_tab_class - return "active" if current_page?(controller: "/projects", action: :edit, id: @project) - - if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name - "active" - end - end - - def branches_tab_class - if current_controller?(:protected_branches) || - current_controller?(:branches) || - current_page?(namespace_project_repository_path(@project.namespace, - @project)) - 'active' - end - end - - # Use nav_tab for save controller/action but different params - def nav_tab(key, value, &block) - o = {} - o[:class] = "" - - if value.nil? - o[:class] << " active" if params[key].blank? - else - o[:class] << " active" if params[key] == value - end - - if block_given? - content_tag(:li, capture(&block), o) - else - content_tag(:li, nil, o) - end - end -end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb deleted file mode 100644 index fb85544df2d..00000000000 --- a/app/helpers/tags_helper.rb +++ /dev/null @@ -1,14 +0,0 @@ -module TagsHelper - def tag_path(tag) - "/tags/#{tag}" - end - - def tag_list(project) - html = '' - project.tag_list.each do |tag| - html << link_to(tag, tag_path(tag)) - end - - html.html_safe - end -end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb deleted file mode 100644 index 03a49e119b8..00000000000 --- a/app/helpers/tree_helper.rb +++ /dev/null @@ -1,88 +0,0 @@ -module TreeHelper - # Sorts a repository's tree so that folders are before files and renders - # their corresponding partials - # - # contents - A Grit::Tree object for the current tree - def render_tree(tree) - # Render Folders before Files/Submodules - folders, files, submodules = tree.trees, tree.blobs, tree.submodules - - tree = "" - - # Render folders if we have any - tree << render(partial: 'projects/tree/tree_item', collection: folders, - locals: { type: 'folder' }) if folders.present? - - # Render files if we have any - tree << render(partial: 'projects/tree/blob_item', collection: files, - locals: { type: 'file' }) if files.present? - - # Render submodules if we have any - tree << render(partial: 'projects/tree/submodule_item', - collection: submodules) if submodules.present? - - tree.html_safe - end - - def render_readme(readme) - render_markup(readme.name, readme.data) - end - - # Return an image icon depending on the file type and mode - # - # type - String type of the tree item; either 'folder' or 'file' - # mode - File unix mode - # name - File name - def tree_icon(type, mode, name) - icon("#{file_type_icon_class(type, mode, name)} fw") - end - - def tree_hex_class(content) - "file_#{hexdigest(content.name)}" - end - - # Simple shortcut to File.join - def tree_join(*args) - File.join(*args) - end - - def allowed_tree_edit?(project = nil, ref = nil) - project ||= @project - ref ||= @ref - return false unless project.repository.branch_names.include?(ref) - - ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) - end - - def tree_breadcrumbs(tree, max_links = 2) - if @path.present? - part_path = "" - parts = @path.split('/') - - yield('..', nil) if parts.count > max_links - - parts.each do |part| - part_path = File.join(part_path, part) unless part_path.empty? - part_path = part if part_path.empty? - - next unless parts.last(2).include?(part) if parts.count > max_links - yield(part, tree_join(@ref, part_path)) - end - end - end - - def up_dir_path - file = File.join(@path, "..") - tree_join(@ref, file) - end - - # returns the relative path of the first subdir that doesn't have only one directory descendant - def flatten_tree(tree) - subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) - if subtree.count == 1 && subtree.first.dir? - return tree_join(tree.name, flatten_tree(subtree.first)) - else - return tree.name - end - end -end diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb deleted file mode 100644 index f64d730b448..00000000000 --- a/app/helpers/version_check_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module VersionCheckHelper - def version_status_badge - if Rails.env.production? - image_tag VersionCheck.new.url - end - end -end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb deleted file mode 100644 index b52cd23aba2..00000000000 --- a/app/helpers/visibility_level_helper.rb +++ /dev/null @@ -1,95 +0,0 @@ -module VisibilityLevelHelper - def visibility_level_color(level) - case level - when Gitlab::VisibilityLevel::PRIVATE - 'vs-private' - when Gitlab::VisibilityLevel::INTERNAL - 'vs-internal' - when Gitlab::VisibilityLevel::PUBLIC - 'vs-public' - end - end - - # Return the description for the +level+ argument. - # - # +level+ One of the Gitlab::VisibilityLevel constants - # +form_model+ Either a model object (Project, Snippet, etc.) or the name of - # a Project or Snippet class. - def visibility_level_description(level, form_model) - case form_model.is_a?(String) ? form_model : form_model.class.name - when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' - snippet_visibility_level_description(level) - when 'Project' - project_visibility_level_description(level) - end - end - - def project_visibility_level_description(level) - capture_haml do - haml_tag :span do - case level - when Gitlab::VisibilityLevel::PRIVATE - haml_concat "Project access must be granted explicitly for each user." - when Gitlab::VisibilityLevel::INTERNAL - haml_concat "The project can be cloned by" - haml_concat "any logged in user." - when Gitlab::VisibilityLevel::PUBLIC - haml_concat "The project can be cloned" - haml_concat "without any" - haml_concat "authentication." - end - end - end - end - - def snippet_visibility_level_description(level) - capture_haml do - haml_tag :span do - case level - when Gitlab::VisibilityLevel::PRIVATE - haml_concat "The snippet is visible only for me." - when Gitlab::VisibilityLevel::INTERNAL - haml_concat "The snippet is visible for any logged in user." - when Gitlab::VisibilityLevel::PUBLIC - haml_concat "The snippet can be accessed" - haml_concat "without any" - haml_concat "authentication." - end - end - end - end - - def visibility_level_icon(level) - case level - when Gitlab::VisibilityLevel::PRIVATE - private_icon - when Gitlab::VisibilityLevel::INTERNAL - internal_icon - when Gitlab::VisibilityLevel::PUBLIC - public_icon - end - end - - def visibility_level_label(level) - Project.visibility_levels.key(level) - end - - def restricted_visibility_levels(show_all = false) - return [] if current_user.is_admin? && !show_all - current_application_settings.restricted_visibility_levels || [] - end - - def default_project_visibility - current_application_settings.default_project_visibility - end - - def default_snippet_visibility - current_application_settings.default_snippet_visibility - end - - def skip_level?(form_model, level) - form_model.is_a?(Project) && - form_model.forked? && - !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level) - end -end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb deleted file mode 100644 index f8a96516e61..00000000000 --- a/app/helpers/wiki_helper.rb +++ /dev/null @@ -1,24 +0,0 @@ -module WikiHelper - # Rails v4.1.9+ escapes all model IDs, converting slashes into %2F. The - # only way around this is to implement our own path generators. - def namespace_project_wiki_path(namespace, project, wiki_page, *args) - slug = - case wiki_page - when Symbol - wiki_page - when String - wiki_page - else - wiki_page.slug - end - namespace_project_path(namespace, project) + "/wikis/#{slug}" - end - - def edit_namespace_project_wiki_path(namespace, project, wiki_page, *args) - namespace_project_wiki_path(namespace, project, wiki_page) + '/edit' - end - - def history_namespace_project_wiki_path(namespace, project, wiki_page, *args) - namespace_project_wiki_path(namespace, project, wiki_page) + '/history' - end -end diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index aedb0889185..2b650bc6eac 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,6 +1,6 @@ class BaseMailer < ActionMailer::Base - add_template_helper ApplicationHelper - add_template_helper GitlabMarkdownHelper + add_template_helper Gitlab::ApplicationHelper + add_template_helper Gitlab::GitlabMarkdownHelper attr_accessor :current_user helper_method :current_user, :can? diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb new file mode 100644 index 00000000000..6fb4fba85e5 --- /dev/null +++ b/app/mailers/ci/emails/builds.rb @@ -0,0 +1,17 @@ +module Ci + module Emails + module Builds + def build_fail_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha)) + end + + def build_success_email(build_id, to) + @build = Ci::Build.find(build_id) + @project = @build.project + mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha)) + end + end + end +end diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb new file mode 100644 index 00000000000..44e490e9b36 --- /dev/null +++ b/app/mailers/ci/notify.rb @@ -0,0 +1,47 @@ +module Ci + class Notify < ActionMailer::Base + include Ci::Emails::Builds + + add_template_helper Ci::ApplicationHelper + add_template_helper Ci::GitlabHelper + + default_url_options[:host] = GitlabCi.config.gitlab_ci.host + default_url_options[:protocol] = GitlabCi.config.gitlab_ci.protocol + default_url_options[:port] = GitlabCi.config.gitlab_ci.port if GitlabCi.config.gitlab_ci_on_non_standard_port? + default_url_options[:script_name] = GitlabCi.config.gitlab_ci.relative_url_root + + default from: GitlabCi.config.gitlab_ci.email_from + + # Just send email with 3 seconds delay + def self.delay + delay_for(2.seconds) + end + + private + + # Formats arguments into a String suitable for use as an email subject + # + # extra - Extra Strings to be inserted into the subject + # + # Examples + # + # >> subject('Lorem ipsum') + # => "GitLab-CI | Lorem ipsum" + # + # # Automatically inserts Project name when @project is set + # >> @project = Project.last + # => # + # >> subject('Lorem ipsum') + # => "GitLab-CI | Ruby on Rails | Lorem ipsum " + # + # # Accepts multiple arguments + # >> subject('Lorem ipsum', 'Dolor sit amet') + # => "GitLab-CI | Lorem ipsum | Dolor sit amet" + def subject(*extra) + subject = "GitLab-CI" + subject << (@project ? " | #{@project.name}" : "") + subject << " | " + extra.join(' | ') if extra.present? + subject + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 5717c89e61d..38afb49c78c 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -8,8 +8,8 @@ class Notify < BaseMailer include Emails::Profile include Emails::Groups - add_template_helper MergeRequestsHelper - add_template_helper EmailsHelper + add_template_helper Gitlab::MergeRequestsHelper + add_template_helper Gitlab::EmailsHelper def test_email(recipient_email, subject, body) mail(to: recipient_email, diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb new file mode 100644 index 00000000000..0ea2452e392 --- /dev/null +++ b/app/models/ci/application_setting.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: application_settings +# +# id :integer not null, primary key +# all_broken_builds :boolean +# add_pusher :boolean +# created_at :datetime +# updated_at :datetime +# + +module Ci + class ApplicationSetting < ActiveRecord::Base + extend Ci::Model + + def self.current + Ci::ApplicationSetting.last + end + + def self.create_from_defaults + create( + all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'], + add_pusher: Ci::Settings.gitlab_ci['add_pusher'], + ) + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb new file mode 100644 index 00000000000..64e7a600672 --- /dev/null +++ b/app/models/ci/build.rb @@ -0,0 +1,285 @@ +# == Schema Information +# +# Table name: builds +# +# id :integer not null, primary key +# project_id :integer +# status :string(255) +# finished_at :datetime +# trace :text +# created_at :datetime +# updated_at :datetime +# started_at :datetime +# runner_id :integer +# commit_id :integer +# coverage :float +# commands :text +# job_id :integer +# name :string(255) +# options :text +# allow_failure :boolean default(FALSE), not null +# stage :string(255) +# deploy :boolean default(FALSE) +# trigger_request_id :integer +# + +module Ci + class Build < ActiveRecord::Base + extend Ci::Model + + LAZY_ATTRIBUTES = ['trace'] + + belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :project, class_name: 'Ci::Project' + belongs_to :runner, class_name: 'Ci::Runner' + belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' + + serialize :options + + validates :commit, presence: true + validates :status, presence: true + validates :coverage, numericality: true, allow_blank: true + + scope :running, ->() { where(status: "running") } + scope :pending, ->() { where(status: "pending") } + scope :success, ->() { where(status: "success") } + scope :failed, ->() { where(status: "failed") } + scope :unstarted, ->() { where(runner_id: nil) } + scope :running_or_pending, ->() { where(status:[:running, :pending]) } + + acts_as_taggable + + # To prevent db load megabytes of data from trace + default_scope -> { select(Ci::Build.columns_without_lazy) } + + class << self + def columns_without_lazy + (column_names - LAZY_ATTRIBUTES).map do |column_name| + "#{table_name}.#{column_name}" + end + end + + def last_month + where('created_at > ?', Date.today - 1.month) + end + + def first_pending + pending.unstarted.order('created_at ASC').first + end + + def create_from(build) + new_build = build.dup + new_build.status = :pending + new_build.runner_id = nil + new_build.save + end + + def retry(build) + new_build = Ci::Build.new(status: :pending) + new_build.options = build.options + new_build.commands = build.commands + new_build.tag_list = build.tag_list + new_build.commit_id = build.commit_id + new_build.project_id = build.project_id + new_build.name = build.name + new_build.allow_failure = build.allow_failure + new_build.stage = build.stage + new_build.trigger_request = build.trigger_request + new_build.save + new_build + end + end + + state_machine :status, initial: :pending do + event :run do + transition pending: :running + end + + event :drop do + transition running: :failed + end + + event :success do + transition running: :success + end + + event :cancel do + transition [:pending, :running] => :canceled + end + + after_transition pending: :running do |build, transition| + build.update_attributes started_at: Time.now + end + + after_transition any => [:success, :failed, :canceled] do |build, transition| + build.update_attributes finished_at: Time.now + project = build.project + + if project.web_hooks? + Ci::WebHookService.new.build_end(build) + end + + if build.commit.success? + build.commit.create_next_builds(build.trigger_request) + end + + project.execute_services(build) + + if project.coverage_enabled? + build.update_coverage + end + end + + state :pending, value: 'pending' + state :running, value: 'running' + state :failed, value: 'failed' + state :success, value: 'success' + state :canceled, value: 'canceled' + end + + delegate :sha, :short_sha, :before_sha, :ref, + to: :commit, prefix: false + + def trace_html + html = Ci::Ansi2html::convert(trace) if trace.present? + html ||= '' + end + + def trace + if project && read_attribute(:trace).present? + read_attribute(:trace).gsub(project.token, 'xxxxxx') + end + end + + def started? + !pending? && !canceled? && started_at + end + + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + + def ignored? + failed? && allow_failure? + end + + def timeout + project.timeout + end + + def variables + yaml_variables + project_variables + trigger_variables + end + + def duration + if started_at && finished_at + finished_at - started_at + elsif started_at + Time.now - started_at + end + end + + def project + commit.project + end + + def project_id + commit.project_id + end + + def project_name + project.name + end + + def repo_url + project.repo_url_with_auth + end + + def allow_git_fetch + project.allow_git_fetch + end + + def update_coverage + coverage = extract_coverage(trace, project.coverage_regex) + + if coverage.is_a? Numeric + update_attributes(coverage: coverage) + end + end + + def extract_coverage(text, regex) + begin + matches = text.gsub(Regexp.new(regex)).to_a.last + coverage = matches.gsub(/\d+(\.\d+)?/).first + + if coverage.present? + coverage.to_f + end + rescue => ex + # if bad regex or something goes wrong we dont want to interrupt transition + # so we just silentrly ignore error for now + end + end + + def trace + if File.exist?(path_to_trace) + File.read(path_to_trace) + else + # backward compatibility + read_attribute :trace + end + end + + def trace=(trace) + unless Dir.exists? dir_to_trace + FileUtils.mkdir_p dir_to_trace + end + + File.write(path_to_trace, trace) + end + + def dir_to_trace + File.join( + Ci::Settings.gitlab_ci.builds_path, + created_at.utc.strftime("%Y_%m"), + project.id.to_s + ) + end + + def path_to_trace + "#{dir_to_trace}/#{id}.log" + end + + private + + def yaml_variables + if commit.config_processor + commit.config_processor.variables.map do |key, value| + { key: key, value: value, public: true } + end + else + [] + end + end + + def project_variables + project.variables.map do |variable| + { key: variable.key, value: variable.value, public: false } + end + end + + def trigger_variables + if trigger_request && trigger_request.variables + trigger_request.variables.map do |key, value| + { key: key, value: value, public: false } + end + else + [] + end + end + end +end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb new file mode 100644 index 00000000000..23cd47dfe37 --- /dev/null +++ b/app/models/ci/commit.rb @@ -0,0 +1,267 @@ +# == Schema Information +# +# Table name: commits +# +# id :integer not null, primary key +# project_id :integer +# ref :string(255) +# sha :string(255) +# before_sha :string(255) +# push_data :text +# created_at :datetime +# updated_at :datetime +# tag :boolean default(FALSE) +# yaml_errors :text +# committed_at :datetime +# + +module Ci + class Commit < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + has_many :builds, dependent: :destroy, class_name: 'Ci::Build' + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + + serialize :push_data + + validates_presence_of :ref, :sha, :before_sha, :push_data + validate :valid_commit_sha + + def self.truncate_sha(sha) + sha[0...8] + end + + def to_param + sha + end + + def last_build + builds.order(:id).last + end + + def retry + builds_without_retry.each do |build| + Ci::Build.retry(build) + end + end + + def valid_commit_sha + if self.sha == Ci::Git::BLANK_SHA + self.errors.add(:sha, " cant be 00000000 (branch removal)") + end + end + + def new_branch? + before_sha == Ci::Git::BLANK_SHA + end + + def compare? + !new_branch? + end + + def git_author_name + commit_data[:author][:name] if commit_data && commit_data[:author] + end + + def git_author_email + commit_data[:author][:email] if commit_data && commit_data[:author] + end + + def git_commit_message + commit_data[:message] if commit_data && commit_data[:message] + end + + def short_before_sha + Ci::Commit.truncate_sha(before_sha) + end + + def short_sha + Ci::Commit.truncate_sha(sha) + end + + def commit_data + push_data[:commits].find do |commit| + commit[:id] == sha + end + rescue + nil + end + + def project_recipients + recipients = project.email_recipients.split(' ') + + if project.email_add_pusher? && push_data[:user_email].present? + recipients << push_data[:user_email] + end + + recipients.uniq + end + + def stage + return unless config_processor + stages = builds_without_retry.select(&:active?).map(&:stage) + config_processor.stages.find { |stage| stages.include? stage } + end + + def create_builds_for_stage(stage, trigger_request) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + builds_attrs = config_processor.builds_for_stage_and_ref(stage, ref, tag) + builds_attrs.map do |build_attrs| + builds.create!({ + project: project, + name: build_attrs[:name], + commands: build_attrs[:script], + tag_list: build_attrs[:tags], + options: build_attrs[:options], + allow_failure: build_attrs[:allow_failure], + stage: build_attrs[:stage], + trigger_request: trigger_request, + }) + end + end + + def create_next_builds(trigger_request) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + stages = builds.where(trigger_request: trigger_request).group_by(&:stage) + + config_processor.stages.any? do |stage| + !stages.include?(stage) && create_builds_for_stage(stage, trigger_request).present? + end + end + + def create_builds(trigger_request = nil) + return if skip_ci? && trigger_request.blank? + return unless config_processor + + config_processor.stages.any? do |stage| + create_builds_for_stage(stage, trigger_request).present? + end + end + + def builds_without_retry + @builds_without_retry ||= + begin + grouped_builds = builds.group_by(&:name) + grouped_builds.map do |name, builds| + builds.sort_by(&:id).last + end + end + end + + def builds_without_retry_sorted + return builds_without_retry unless config_processor + + stages = config_processor.stages + builds_without_retry.sort_by do |build| + [stages.index(build.stage) || -1, build.name || ""] + end + end + + def retried_builds + @retried_builds ||= (builds.order(id: :desc) - builds_without_retry) + end + + def status + if skip_ci? + return 'skipped' + elsif yaml_errors.present? + return 'failed' + elsif builds.none? + return 'skipped' + elsif success? + 'success' + elsif pending? + 'pending' + elsif running? + 'running' + elsif canceled? + 'canceled' + else + 'failed' + end + end + + def pending? + builds_without_retry.all? do |build| + build.pending? + end + end + + def running? + builds_without_retry.any? do |build| + build.running? || build.pending? + end + end + + def success? + builds_without_retry.all? do |build| + build.success? || build.ignored? + end + end + + def failed? + status == 'failed' + end + + def canceled? + builds_without_retry.all? do |build| + build.canceled? + end + end + + def duration + @duration ||= builds_without_retry.select(&:duration).sum(&:duration).to_i + end + + def finished_at + @finished_at ||= builds.order('finished_at DESC').first.try(:finished_at) + end + + def coverage + if project.coverage_enabled? + coverage_array = builds_without_retry.map(&:coverage).compact + if coverage_array.size >= 1 + '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) + end + end + end + + def matrix? + builds_without_retry.size > 1 + end + + def config_processor + @config_processor ||= Ci::GitlabCiYamlProcessor.new(push_data[:ci_yaml_file] || project.generated_yaml_config) + rescue Ci::GitlabCiYamlProcessor::ValidationError => e + save_yaml_error(e.message) + nil + rescue Exception => e + logger.error e.message + "\n" + e.backtrace.join("\n") + save_yaml_error("Undefined yaml error") + nil + end + + def skip_ci? + return false if builds.any? + commits = push_data[:commits] + commits.present? && commits.last[:message] =~ /(\[ci skip\])/ + end + + def update_committed! + update!(committed_at: DateTime.now) + end + + private + + def save_yaml_error(error) + return if self.yaml_errors? + self.yaml_errors = error + save + end + end +end diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb new file mode 100644 index 00000000000..cac3a7a49c1 --- /dev/null +++ b/app/models/ci/event.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: events +# +# id :integer not null, primary key +# project_id :integer +# user_id :integer +# is_admin :integer +# description :text +# created_at :datetime +# updated_at :datetime +# + +module Ci + class Event < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + + validates :description, + presence: true, + length: { in: 5..200 } + + scope :admin, ->(){ where(is_admin: true) } + scope :project_wide, ->(){ where(is_admin: false) } + end +end diff --git a/app/models/ci/network.rb b/app/models/ci/network.rb new file mode 100644 index 00000000000..c307907e6b8 --- /dev/null +++ b/app/models/ci/network.rb @@ -0,0 +1,122 @@ +module Ci + class Network + class UnauthorizedError < StandardError; end + + include HTTParty + + API_PREFIX = '/api/v3/' + + def authenticate(api_opts) + opts = { + query: api_opts + } + + endpoint = File.join(url, API_PREFIX, 'user') + response = self.class.get(endpoint, default_opts.merge(opts)) + + build_response(response) + end + + def projects(api_opts, scope = :owned) + # Dont load archived projects + api_opts.merge!(archived: false) + + opts = { + query: api_opts + } + + query = if scope == :owned + 'projects/owned.json' + else + 'projects.json' + end + + endpoint = File.join(url, API_PREFIX, query) + response = self.class.get(endpoint, default_opts.merge(opts)) + + build_response(response) + end + + def project(api_opts, project_id) + opts = { + query: api_opts + } + + query = "projects/#{project_id}.json" + + endpoint = File.join(url, API_PREFIX, query) + response = self.class.get(endpoint, default_opts.merge(opts)) + + build_response(response) + end + + def project_hooks(api_opts, project_id) + opts = { + query: api_opts + } + + query = "projects/#{project_id}/hooks.json" + + endpoint = File.join(url, API_PREFIX, query) + response = self.class.get(endpoint, default_opts.merge(opts)) + + build_response(response) + end + + def enable_ci(project_id, data, api_opts) + opts = { + body: data.to_json, + query: api_opts + } + + query = "projects/#{project_id}/services/gitlab-ci.json" + endpoint = File.join(url, API_PREFIX, query) + response = self.class.put(endpoint, default_opts.merge(opts)) + + case response.code + when 200 + true + when 401 + raise UnauthorizedError + else + nil + end + end + + def disable_ci(project_id, api_opts) + opts = { + query: api_opts + } + + query = "projects/#{project_id}/services/gitlab-ci.json" + + endpoint = File.join(url, API_PREFIX, query) + response = self.class.delete(endpoint, default_opts.merge(opts)) + + build_response(response) + end + + private + + def url + GitlabCi.config.gitlab_server.url + end + + def default_opts + { + headers: { "Content-Type" => "application/json" }, + } + end + + def build_response(response) + case response.code + when 200 + response.parsed_response + when 401 + raise UnauthorizedError + else + nil + end + end + end +end diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb new file mode 100644 index 00000000000..dceca7a275a --- /dev/null +++ b/app/models/ci/project.rb @@ -0,0 +1,221 @@ +# == Schema Information +# +# Table name: projects +# +# id :integer not null, primary key +# name :string(255) not null +# timeout :integer default(3600), not null +# created_at :datetime +# updated_at :datetime +# token :string(255) +# default_ref :string(255) +# path :string(255) +# always_build :boolean default(FALSE), not null +# polling_interval :integer +# public :boolean default(FALSE), not null +# ssh_url_to_repo :string(255) +# gitlab_id :integer +# allow_git_fetch :boolean default(TRUE), not null +# email_recipients :string(255) default(""), not null +# email_add_pusher :boolean default(TRUE), not null +# email_only_broken_builds :boolean default(TRUE), not null +# skip_refs :string(255) +# coverage_regex :string(255) +# shared_runners_enabled :boolean default(FALSE) +# generated_yaml_config :text +# + +module Ci + class Project < ActiveRecord::Base + extend Ci::Model + + include Ci::ProjectStatus + + has_many :commits, ->() { order(:committed_at) }, dependent: :destroy, class_name: 'Ci::Commit' + has_many :builds, through: :commits, dependent: :destroy, class_name: 'Ci::Build' + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' + has_many :runners, through: :runner_projects, class_name: 'Ci::Runner' + has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook' + has_many :events, dependent: :destroy, class_name: 'Ci::Event' + has_many :variables, dependent: :destroy, class_name: 'Ci::Variable' + has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger' + + # Project services + has_many :services, dependent: :destroy, class_name: 'Ci::Service' + has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService' + has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService' + has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService' + + accepts_nested_attributes_for :variables, allow_destroy: true + + # + # Validations + # + validates_presence_of :name, :timeout, :token, :default_ref, + :path, :ssh_url_to_repo, :gitlab_id + + validates_uniqueness_of :gitlab_id + + validates :polling_interval, + presence: true, + if: ->(project) { project.always_build.present? } + + scope :public_only, ->() { where(public: true) } + + before_validation :set_default_values + + class << self + include Ci::CurrentSettings + + def base_build_script + <<-eos + git submodule update --init + ls -la + eos + end + + def parse(project) + params = { + name: project.name_with_namespace, + gitlab_id: project.id, + path: project.path_with_namespace, + default_ref: project.default_branch || 'master', + ssh_url_to_repo: project.ssh_url_to_repo, + email_add_pusher: current_application_settings.add_pusher, + email_only_broken_builds: current_application_settings.all_broken_builds, + } + + project = Ci::Project.new(params) + project.build_missing_services + project + end + + def from_gitlab(user, scope = :owned, options) + opts = user.authenticate_options + opts.merge! options + + projects = Ci::Network.new.projects(opts.compact, scope) + + if projects + projects.map { |pr| OpenStruct.new(pr) } + else + [] + end + end + + def already_added?(project) + where(gitlab_id: project.id).any? + end + + def unassigned(runner) + joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \ + "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}"). + where('#{Ci::RunnerProject.table_name}.project_id' => nil) + end + + def ordered_by_last_commit_date + last_commit_subquery = "(SELECT project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY project_id)" + joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.id = last_commit.project_id"). + order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC") + end + + def search(query) + where("LOWER(#{Ci::Project.table_name}.name) LIKE :query", + query: "%#{query.try(:downcase)}%") + end + end + + def any_runners? + if runners.active.any? + return true + end + + shared_runners_enabled && Ci::Runner.shared.active.any? + end + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def tracked_refs + @tracked_refs ||= default_ref.split(",").map{|ref| ref.strip} + end + + def valid_token? token + self.token && self.token == token + end + + def no_running_builds? + # Get running builds not later than 3 days ago to ignore hangs + builds.running.where("updated_at > ?", 3.days.ago).empty? + end + + def email_notification? + email_add_pusher || email_recipients.present? + end + + def web_hooks? + web_hooks.any? + end + + def services? + services.any? + end + + def timeout_in_minutes + timeout / 60 + end + + def timeout_in_minutes=(value) + self.timeout = value.to_i * 60 + end + + def coverage_enabled? + coverage_regex.present? + end + + # Build a clone-able repo url + # using http and basic auth + def repo_url_with_auth + auth = "gitlab-ci-token:#{token}@" + url = gitlab_url + ".git" + url.sub(/^https?:\/\//) do |prefix| + prefix + auth + end + end + + def available_services_names + %w(slack mail hip_chat) + end + + def build_missing_services + available_services_names.each do |service_name| + service = services.find { |service| service.to_param == service_name } + + # If service is available but missing in db + # we should create an instance. Ex `create_gitlab_ci_service` + service = self.send :"create_#{service_name}_service" if service.nil? + end + end + + def execute_services(data) + services.each do |service| + + # Call service hook only if it is active + begin + service.execute(data) if service.active && service.can_execute?(data) + rescue => e + logger.error(e) + end + end + end + + def gitlab_url + File.join(GitlabCi.config.gitlab_server.url, path) + end + + def setup_finished? + commits.any? + end + end +end diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb new file mode 100644 index 00000000000..6d5cafe81a2 --- /dev/null +++ b/app/models/ci/project_status.rb @@ -0,0 +1,47 @@ +module Ci + module ProjectStatus + def status + last_commit.status if last_commit + end + + def broken? + last_commit.failed? if last_commit + end + + def success? + last_commit.success? if last_commit + end + + def broken_or_success? + broken? || success? + end + + def last_commit + @last_commit ||= commits.last if commits.any? + end + + def last_commit_date + last_commit.try(:created_at) + end + + def human_status + status + end + + # only check for toggling build status within same ref. + def last_commit_changed_status? + ref = last_commit.ref + last_commits = commits.where(ref: ref).last(2) + + if last_commits.size < 2 + false + else + last_commits[0].status != last_commits[1].status + end + end + + def last_commit_for_ref(ref) + commits.where(ref: ref).last + end + end +end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb new file mode 100644 index 00000000000..79c81df5eb2 --- /dev/null +++ b/app/models/ci/runner.rb @@ -0,0 +1,80 @@ +# == Schema Information +# +# Table name: runners +# +# id :integer not null, primary key +# token :string(255) +# created_at :datetime +# updated_at :datetime +# description :string(255) +# contacted_at :datetime +# active :boolean default(TRUE), not null +# is_shared :boolean default(FALSE) +# name :string(255) +# version :string(255) +# revision :string(255) +# platform :string(255) +# architecture :string(255) +# + +module Ci + class Runner < ActiveRecord::Base + extend Ci::Model + + has_many :builds, class_name: 'Ci::Build' + has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' + has_many :projects, through: :runner_projects, class_name: 'Ci::Project' + + has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' + + before_validation :set_default_values + + scope :specific, ->() { where(is_shared: false) } + scope :shared, ->() { where(is_shared: true) } + scope :active, ->() { where(active: true) } + scope :paused, ->() { where(active: false) } + + acts_as_taggable + + def self.search(query) + where('LOWER(runners.token) LIKE :query OR LOWER(runners.description) like :query', + query: "%#{query.try(:downcase)}%") + end + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def assign_to(project, current_user = nil) + self.is_shared = false if shared? + self.save + project.runner_projects.create!(runner_id: self.id) + end + + def display_name + return token unless !description.blank? + + description + end + + def shared? + is_shared + end + + def belongs_to_one_project? + runner_projects.count == 1 + end + + def specific? + !shared? + end + + def only_for?(project) + projects == [project] + end + + def short_sha + token[0...10] + end + end +end diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb new file mode 100644 index 00000000000..44453ee4b41 --- /dev/null +++ b/app/models/ci/runner_project.rb @@ -0,0 +1,21 @@ +# == Schema Information +# +# Table name: runner_projects +# +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +module Ci + class RunnerProject < ActiveRecord::Base + extend Ci::Model + + belongs_to :runner, class_name: 'Ci::Runner' + belongs_to :project, class_name: 'Ci::Project' + + validates_uniqueness_of :runner_id, scope: :project_id + end +end diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb new file mode 100644 index 00000000000..ed5e3f940b6 --- /dev/null +++ b/app/models/ci/service.rb @@ -0,0 +1,105 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +# To add new service you should build a class inherited from Service +# and implement a set of methods +module Ci + class Service < ActiveRecord::Base + extend Ci::Model + + serialize :properties, JSON + + default_value_for :active, false + + after_initialize :initialize_properties + + belongs_to :project, class_name: 'Ci::Project' + + validates :project_id, presence: true + + def activated? + active + end + + def category + :common + end + + def initialize_properties + self.properties = {} if properties.nil? + end + + def title + # implement inside child + end + + def description + # implement inside child + end + + def help + # implement inside child + end + + def to_param + # implement inside child + end + + def fields + # implement inside child + [] + end + + def can_test? + project.builds.any? + end + + def can_execute?(build) + true + end + + def execute(build) + # implement inside child + end + + # Provide convenient accessor methods + # for each serialized property. + def self.prop_accessor(*args) + args.each do |arg| + class_eval %{ + def #{arg} + (properties || {})['#{arg}'] + end + + def #{arg}=(value) + self.properties ||= {} + self.properties['#{arg}'] = value + end + } + end + end + + def self.boolean_accessor(*args) + self.prop_accessor(*args) + + args.each do |arg| + class_eval %{ + def #{arg}? + ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + end + } + end + end + end +end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb new file mode 100644 index 00000000000..84eab91e8ba --- /dev/null +++ b/app/models/ci/trigger.rb @@ -0,0 +1,39 @@ +# == Schema Information +# +# Table name: triggers +# +# id :integer not null, primary key +# token :string(255) +# project_id :integer not null +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# + +module Ci + class Trigger < ActiveRecord::Base + extend Ci::Model + + acts_as_paranoid + + belongs_to :project, class_name: 'Ci::Trigger' + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + + validates_presence_of :token + validates_uniqueness_of :token + + before_validation :set_default_values + + def set_default_values + self.token = SecureRandom.hex(15) if self.token.blank? + end + + def last_trigger_request + trigger_requests.last + end + + def short_token + token[0...10] + end + end +end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb new file mode 100644 index 00000000000..29cd9553394 --- /dev/null +++ b/app/models/ci/trigger_request.rb @@ -0,0 +1,23 @@ +# == Schema Information +# +# Table name: trigger_requests +# +# id :integer not null, primary key +# trigger_id :integer not null +# variables :text +# created_at :datetime +# updated_at :datetime +# commit_id :integer +# + +module Ci + class TriggerRequest < ActiveRecord::Base + extend Ci::Model + + belongs_to :trigger, class_name: 'Ci::Trigger' + belongs_to :commit, class_name: 'Ci::Commit' + has_many :builds, class_name: 'Ci::Build' + + serialize :variables + end +end diff --git a/app/models/ci/user.rb b/app/models/ci/user.rb new file mode 100644 index 00000000000..7456bd1a77b --- /dev/null +++ b/app/models/ci/user.rb @@ -0,0 +1,97 @@ +# User object is stored in session +module Ci + class User + DEVELOPER_ACCESS = 30 + + attr_reader :attributes + + def initialize(hash) + @attributes = hash + end + + def gitlab_projects(search = nil, page = 1, per_page = 100) + Rails.cache.fetch(cache_key(page, per_page, search)) do + Ci::Project.from_gitlab(self, :authorized, { page: page, per_page: per_page, search: search, ci_enabled_first: true }) + end + end + + def method_missing(meth, *args, &block) + if attributes.has_key?(meth.to_s) + attributes[meth.to_s] + else + super + end + end + + def avatar_url + attributes['avatar_url'] + end + + def cache_key(*args) + "#{self.id}:#{args.join(":")}:#{sync_at.to_s}" + end + + def sync_at + @sync_at ||= Time.now + end + + def reset_cache + @sync_at = Time.now + end + + def can_access_project?(project_gitlab_id) + !!project_info(project_gitlab_id) + end + + # Indicate if user has developer access or higher + def has_developer_access?(project_gitlab_id) + data = project_info(project_gitlab_id) + + return false unless data && data["permissions"] + + permissions = data["permissions"] + + if permissions["project_access"] && permissions["project_access"]["access_level"] >= DEVELOPER_ACCESS + return true + end + + if permissions["group_access"] && permissions["group_access"]["access_level"] >= DEVELOPER_ACCESS + return true + end + end + + def can_manage_project?(project_gitlab_id) + Rails.cache.fetch(cache_key('manage', project_gitlab_id, sync_at)) do + !!Ci::Network.new.project_hooks(authenticate_options, project_gitlab_id) + end + end + + def authorized_runners + Ci::Runner.specific.includes(:runner_projects). + where(runner_projects: { project_id: authorized_projects } ) + end + + def authorized_projects + Ci::Project.where(gitlab_id: gitlab_projects.map(&:id)).select do |project| + # This is slow: it makes request to GitLab for each project to verify manage permission + can_manage_project?(project.gitlab_id) + end + end + + def authenticate_options + if attributes['access_token'] + { access_token: attributes['access_token'] } + else + { private_token: attributes['private_token'] } + end + end + + private + + def project_info(project_gitlab_id) + Rails.cache.fetch(cache_key("project_info", project_gitlab_id, sync_at)) do + Ci::Network.new.project(authenticate_options, project_gitlab_id) + end + end + end +end diff --git a/app/models/ci/user_session.rb b/app/models/ci/user_session.rb new file mode 100644 index 00000000000..27c71e30591 --- /dev/null +++ b/app/models/ci/user_session.rb @@ -0,0 +1,23 @@ +module Ci + class UserSession + include ActiveModel::Conversion + include Ci::StaticModel + extend ActiveModel::Naming + + def authenticate(auth_opts) + network = Ci::Network.new + user = network.authenticate(auth_opts) + + if user + user["access_token"] = auth_opts[:access_token] + return Ci::User.new(user) + else + nil + end + + user + rescue + nil + end + end +end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb new file mode 100644 index 00000000000..7a542802fa6 --- /dev/null +++ b/app/models/ci/variable.rb @@ -0,0 +1,25 @@ +# == Schema Information +# +# Table name: variables +# +# id :integer not null, primary key +# project_id :integer not null +# key :string(255) +# value :text +# encrypted_value :text +# encrypted_value_salt :string(255) +# encrypted_value_iv :string(255) +# + +module Ci + class Variable < ActiveRecord::Base + extend Ci::Model + + belongs_to :project, class_name: 'Ci::Project' + + validates_presence_of :key + validates_uniqueness_of :key, scope: :project_id + + attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + end +end diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb new file mode 100644 index 00000000000..4b8c65a1a65 --- /dev/null +++ b/app/models/ci/web_hook.rb @@ -0,0 +1,44 @@ +# == Schema Information +# +# Table name: web_hooks +# +# id :integer not null, primary key +# url :string(255) not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + +module Ci + class WebHook < ActiveRecord::Base + extend Ci::Model + + include HTTParty + + belongs_to :project, class_name: 'Ci::WebHook' + + # HTTParty timeout + default_timeout 10 + + validates :url, presence: true, + format: { with: URI::regexp(%w(http https)), message: "should be a valid url" } + + def execute(data) + parsed_url = URI.parse(url) + if parsed_url.userinfo.blank? + Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false) + else + post_url = url.gsub("#{parsed_url.userinfo}@", "") + auth = { + username: URI.decode(parsed_url.user), + password: URI.decode(parsed_url.password), + } + Ci::WebHook.post(post_url, + body: data.to_json, + headers: { "Content-Type" => "application/json" }, + verify: false, + basic_auth: auth) + end + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 69f9af91c51..f14cd884c89 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -316,7 +316,7 @@ def to_reference(_from_project = nil) end def web_url - Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self) + Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -433,7 +433,7 @@ def avatar_url if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self) + Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self) end end diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb new file mode 100644 index 00000000000..3e9f99e7eaf --- /dev/null +++ b/app/models/project_services/ci/hip_chat_message.rb @@ -0,0 +1,78 @@ +module Ci + class HipChatMessage + attr_reader :build + + def initialize(build) + @build = build + end + + def to_s + lines = Array.new + lines.push("#{project.name} - ") + + if commit.matrix? + lines.push("Commit ##{commit.id}
    ") + else + first_build = commit.builds_without_retry.first + lines.push("Build '#{first_build.name}' ##{first_build.id}
    ") + end + + lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}
    ") + lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).") + lines.join('') + end + + def status_color(build_or_commit=nil) + build_or_commit ||= commit_status + case build_or_commit + when :success + 'green' + when :failed, :canceled + 'red' + else # :pending, :running or unknown + 'yellow' + end + end + + def notify? + [:failed, :canceled].include?(commit_status) + end + + + private + + def commit + build.commit + end + + def project + commit.project + end + + def build_status + build.status.to_sym + end + + def commit_status + commit.status.to_sym + end + + def humanized_status(build_or_commit=nil) + build_or_commit ||= commit_status + case build_or_commit + when :pending + "Pending" + when :running + "Running" + when :failed + "Failed" + when :success + "Successful" + when :canceled + "Canceled" + else + "Unknown" + end + end + end +end diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb new file mode 100644 index 00000000000..68acf71251e --- /dev/null +++ b/app/models/project_services/ci/hip_chat_service.rb @@ -0,0 +1,93 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +module Ci + class HipChatService < Service + prop_accessor :hipchat_token, :hipchat_room, :hipchat_server + boolean_accessor :notify_only_broken_builds + validates :hipchat_token, presence: true, if: :activated? + validates :hipchat_room, presence: true, if: :activated? + default_value_for :notify_only_broken_builds, true + + def title + "HipChat" + end + + def description + "Private group chat, video chat, instant messaging for teams" + end + + def help + end + + def to_param + 'hip_chat' + end + + def fields + [ + { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' }, + { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' }, + { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' }, + { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include? build + + case commit.status.to_sym + when :failed + true + when :success + true unless notify_only_broken_builds? + else + false + end + end + + def execute(build) + msg = Ci::HipChatMessage.new(build) + opts = default_options.merge( + token: hipchat_token, + room: hipchat_room, + server: server_url, + color: msg.status_color, + notify: msg.notify? + ) + Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts) + end + + private + + def default_options + { + service_name: 'GitLab CI', + message_format: 'html' + } + end + + def server_url + if hipchat_server.blank? + 'https://api.hipchat.com' + else + hipchat_server + end + end + end +end diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb new file mode 100644 index 00000000000..3619a50fa96 --- /dev/null +++ b/app/models/project_services/ci/mail_service.rb @@ -0,0 +1,84 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +module Ci + class MailService < Service + delegate :email_recipients, :email_recipients=, + :email_add_pusher, :email_add_pusher=, + :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false + + before_save :update_project + + default_value_for :active, true + + def title + 'Mail' + end + + def description + 'Email notification' + end + + def to_param + 'mail' + end + + def fields + [ + { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' }, + { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' }, + { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + # it doesn't make sense to send emails for retried builds + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include?(build) + + case build.status.to_sym + when :failed + true + when :success + true unless email_only_broken_builds + else + false + end + end + + def execute(build) + build.commit.project_recipients.each do |recipient| + case build.status.to_sym + when :success + mailer.build_success_email(build.id, recipient) + when :failed + mailer.build_fail_email(build.id, recipient) + end + end + end + + private + + def update_project + project.save! + end + + def mailer + Ci::Notify.delay + end + end +end diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb new file mode 100644 index 00000000000..7d884849bf3 --- /dev/null +++ b/app/models/project_services/ci/slack_message.rb @@ -0,0 +1,97 @@ +require 'slack-notifier' + +module Ci + class SlackMessage + def initialize(commit) + @commit = commit + end + + def pretext + '' + end + + def color + attachment_color + end + + def fallback + format(attachment_message) + end + + def attachments + fields = [] + + if commit.matrix? + commit.builds_without_retry.each do |build| + next if build.allow_failure? + next unless build.failed? + fields << { + title: build.name, + value: "Build <#{Ci::RoutesHelper.ci_project_build_url(project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)." + } + end + end + + [{ + text: attachment_message, + color: attachment_color, + fields: fields + }] + end + + private + + attr_reader :commit + + def attachment_message + out = "<#{Ci::RoutesHelper.ci_project_url(project)}|#{project_name}>: " + if commit.matrix? + out << "Commit <#{Ci::RoutesHelper.ci_project_ref_commit_url(project, commit.ref, commit.sha)}|\##{commit.id}> " + else + build = commit.builds_without_retry.first + out << "Build <#{Ci::RoutesHelper.ci_project_build_url(project, build)}|\##{build.id}> " + end + out << "(<#{commit_sha_link}|#{commit.short_sha}>) " + out << "of <#{commit_ref_link}|#{commit.ref}> " + out << "by #{commit.git_author_name} " if commit.git_author_name + out << "#{commit_status} in " + out << "#{commit.duration} second(s)" + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def project + commit.project + end + + def project_name + project.name + end + + def commit_sha_link + "#{project.gitlab_url}/commit/#{commit.sha}" + end + + def commit_ref_link + "#{project.gitlab_url}/commits/#{commit.ref}" + end + + def attachment_color + if commit.success? + 'good' + else + 'danger' + end + end + + def commit_status + if commit.success? + 'succeeded' + else + 'failed' + end + end + end +end diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb new file mode 100644 index 00000000000..c9a7f865a25 --- /dev/null +++ b/app/models/project_services/ci/slack_service.rb @@ -0,0 +1,81 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +module Ci + class SlackService < Service + prop_accessor :webhook + boolean_accessor :notify_only_broken_builds + validates :webhook, presence: true, if: :activated? + + default_value_for :notify_only_broken_builds, true + + def title + 'Slack' + end + + def description + 'A team communication tool for the 21st century' + end + + def to_param + 'slack' + end + + def help + 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present? + end + + def fields + [ + { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' }, + { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' } + ] + end + + def can_execute?(build) + return if build.allow_failure? + + commit = build.commit + return unless commit + return unless commit.builds_without_retry.include?(build) + + case commit.status.to_sym + when :failed + true + when :success + true unless notify_only_broken_builds? + else + false + end + end + + def execute(build) + message = Ci::SlackMessage.new(build.commit) + options = default_options.merge( + color: message.color, + fallback: message.fallback, + attachments: message.attachments + ) + Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options) + end + + private + + def default_options + { + username: 'GitLab CI' + } + end + end +end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 0ebc0a3ba1a..9558292fea3 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -19,7 +19,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index bfa8fc7b860..35e30b1cb0b 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -19,7 +19,7 @@ # class JiraService < IssueTrackerService - include Rails.application.routes.url_helpers + include Gitlab::Application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb new file mode 100644 index 00000000000..0a1abf89a95 --- /dev/null +++ b/app/services/ci/create_commit_service.rb @@ -0,0 +1,50 @@ +module Ci + class CreateCommitService + def execute(project, params) + before_sha = params[:before] + sha = params[:checkout_sha] || params[:after] + origin_ref = params[:ref] + + unless origin_ref && sha.present? + return false + end + + ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '') + + # Skip branch removal + if sha == Ci::Git::BLANK_SHA + return false + end + + commit = project.commits.find_by_sha_and_ref(sha, ref) + + # Create commit if not exists yet + unless commit + data = { + ref: ref, + sha: sha, + tag: origin_ref.start_with?('refs/tags/'), + before_sha: before_sha, + push_data: { + before: before_sha, + after: sha, + ref: ref, + user_name: params[:user_name], + user_email: params[:user_email], + repository: params[:repository], + commits: params[:commits], + total_commits_count: params[:total_commits_count], + ci_yaml_file: params[:ci_yaml_file] + } + } + + commit = project.commits.create(data) + end + + commit.update_committed! + commit.create_builds unless commit.builds.any? + + commit + end + end +end diff --git a/app/services/ci/create_project_service.rb b/app/services/ci/create_project_service.rb new file mode 100644 index 00000000000..049ac2e9181 --- /dev/null +++ b/app/services/ci/create_project_service.rb @@ -0,0 +1,35 @@ +module Ci + class CreateProjectService + include Gitlab::Application.routes.url_helpers + + def execute(current_user, params, project_route, forked_project = nil) + @project = Ci::Project.parse(params) + + Ci::Project.transaction do + @project.save! + + data = { + token: @project.token, + project_url: project_route.gsub(":project_id", @project.id.to_s), + } + + unless Ci::Network.new.enable_ci(@project.gitlab_id, data, current_user.authenticate_options) + raise ActiveRecord::Rollback + end + end + + if forked_project + # Copy settings + settings = forked_project.attributes.select do |attr_name, value| + ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name + end + + @project.update(settings) + end + + Ci::EventService.new.create_project(current_user, @project) + + @project + end + end +end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb new file mode 100644 index 00000000000..9bad09f2f54 --- /dev/null +++ b/app/services/ci/create_trigger_request_service.rb @@ -0,0 +1,17 @@ +module Ci + class CreateTriggerRequestService + def execute(project, trigger, ref, variables = nil) + commit = project.commits.where(ref: ref).last + return unless commit + + trigger_request = trigger.trigger_requests.create!( + commit: commit, + variables: variables + ) + + if commit.create_builds(trigger_request) + trigger_request + end + end + end +end diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb new file mode 100644 index 00000000000..3f4e02dd26c --- /dev/null +++ b/app/services/ci/event_service.rb @@ -0,0 +1,31 @@ +module Ci + class EventService + def remove_project(user, project) + create( + description: "Project \"#{project.name}\" has been removed by #{user.username}", + user_id: user.id, + is_admin: true + ) + end + + def create_project(user, project) + create( + description: "Project \"#{project.name}\" has been created by #{user.username}", + user_id: user.id, + is_admin: true + ) + end + + def change_project_settings(user, project) + create( + project_id: project.id, + user_id: user.id, + description: "User \"#{user.username}\" updated projects settings" + ) + end + + def create(*args) + Ci::Event.create!(*args) + end + end +end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb new file mode 100644 index 00000000000..b95835ba093 --- /dev/null +++ b/app/services/ci/image_for_build_service.rb @@ -0,0 +1,31 @@ +module Ci + class ImageForBuildService + def execute(project, params) + image_name = + if params[:sha] + commit = project.commits.find_by(sha: params[:sha]) + image_for_commit(commit) + elsif params[:ref] + commit = project.last_commit_for_ref(params[:ref]) + image_for_commit(commit) + else + 'build-unknown.svg' + end + + image_path = Rails.root.join('public/ci', image_name) + + OpenStruct.new( + path: image_path, + name: image_name + ) + end + + private + + def image_for_commit(commit) + return 'build-unknown.svg' unless commit + + 'build-' + commit.status + ".svg" + end + end +end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb new file mode 100644 index 00000000000..7e0b58a5dc9 --- /dev/null +++ b/app/services/ci/register_build_service.rb @@ -0,0 +1,40 @@ +module Ci + # This class responsible for assigning + # proper pending build to runner on runner API request + class RegisterBuildService + def execute(current_runner) + builds = Ci::Build.pending.unstarted + + builds = + if current_runner.shared? + # don't run projects which have not enables shared runners + builds.includes(:project).where(projects: { shared_runners_enabled: true }) + else + # do run projects which are only assigned to this runner + builds.where(project_id: current_runner.projects) + end + + builds = builds.order('created_at ASC') + + build = builds.find do |build| + (build.tag_list - current_runner.tag_list).empty? + end + + + if build + # In case when 2 runners try to assign the same build, second runner will be declined + # with StateMachine::InvalidTransition in run! method. + build.with_lock do + build.runner_id = current_runner.id + build.save! + build.run! + end + end + + build + + rescue StateMachine::InvalidTransition + nil + end + end +end diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb new file mode 100644 index 00000000000..3a17596aaeb --- /dev/null +++ b/app/services/ci/test_hook_service.rb @@ -0,0 +1,7 @@ +module Ci + class TestHookService + def execute(hook, current_user) + Ci::WebHookService.new.build_end(hook.project.commits.last.last_build) + end + end +end diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb new file mode 100644 index 00000000000..87984b20fa1 --- /dev/null +++ b/app/services/ci/web_hook_service.rb @@ -0,0 +1,36 @@ +module Ci + class WebHookService + def build_end(build) + execute_hooks(build.project, build_data(build)) + end + + def execute_hooks(project, data) + project.web_hooks.each do |web_hook| + async_execute_hook(web_hook, data) + end + end + + def async_execute_hook(hook, data) + Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data) + end + + def build_data(build) + project = build.project + data = {} + data.merge!({ + build_id: build.id, + build_name: build.name, + build_status: build.status, + build_started_at: build.started_at, + build_finished_at: build.finished_at, + project_id: project.id, + project_name: project.name, + gitlab_url: project.gitlab_url, + ref: build.ref, + sha: build.sha, + before_sha: build.before_sha, + push_data: build.commit.push_data + }) + end + end +end diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml new file mode 100644 index 00000000000..634c9daa477 --- /dev/null +++ b/app/views/ci/admin/application_settings/_form.html.haml @@ -0,0 +1,24 @@ += form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + - if @application_setting.errors.any? + #error_explanation + .alert.alert-danger + - @application_setting.errors.full_messages.each do |msg| + %p= msg + + %fieldset + %legend Default Project Settings + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :all_broken_builds do + = f.check_box :all_broken_builds + Send emails only on broken builds + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :add_pusher do + = f.check_box :add_pusher + Add pusher to recipients list + + .form-actions + = f.submit 'Save', class: 'btn btn-primary' diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml new file mode 100644 index 00000000000..7ef0aa89ed6 --- /dev/null +++ b/app/views/ci/admin/application_settings/show.html.haml @@ -0,0 +1,3 @@ +%h3.page-title Settings +%hr += render 'form' diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml new file mode 100644 index 00000000000..1766ca39760 --- /dev/null +++ b/app/views/ci/admin/builds/_build.html.haml @@ -0,0 +1,32 @@ +- if build.commit && build.project + %tr.build.alert{class: build_status_alert_class(build)} + %td.build-link + = link_to ci_build_url(build) do + %strong #{build.id} + + %td.status + = build.status + + %td.commit-link + = commit_link(build.commit) + + %td.runner + - if build.runner + = link_to build.runner.id, ci_admin_runner_path(build.runner) + + %td.build-project + = truncate build.project.name, length: 30 + + %td.build-message + %span= truncate(build.commit.git_commit_message, length: 30) + + %td.build-branch + %span= truncate(build.ref, length: 25) + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml new file mode 100644 index 00000000000..ab4ced54327 --- /dev/null +++ b/app/views/ci/admin/builds/index.html.haml @@ -0,0 +1,27 @@ +%ul.nav.nav-tabs.append-bottom-20 + %li{class: ("active" if @scope.nil?)} + = link_to 'All builds', ci_admin_builds_path + + %li{class: ("active" if @scope == "pending")} + = link_to "Pending", ci_admin_builds_path(scope: :pending) + + %li{class: ("active" if @scope == "running")} + = link_to "Running", ci_admin_builds_path(scope: :running) + + +%table.builds + %thead + %tr + %th Build + %th Status + %th Commit + %th Runner + %th Project + %th Message + %th Branch + %th Duration + %th Finished at + + = render @builds + += paginate @builds diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml new file mode 100644 index 00000000000..f9ab0994304 --- /dev/null +++ b/app/views/ci/admin/events/index.html.haml @@ -0,0 +1,17 @@ +%table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago + += paginate @events \ No newline at end of file diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml new file mode 100644 index 00000000000..e64bfe853d7 --- /dev/null +++ b/app/views/ci/admin/projects/_project.html.haml @@ -0,0 +1,28 @@ +- last_commit = project.last_commit +%tr.alert{class: commit_status_alert_class(last_commit) } + %td + = project.id + %td + = link_to project do + %strong= project.name + %td + - if last_commit + #{last_commit.status} (#{commit_link(last_commit)}) + - if project.last_commit_date + = time_ago_in_words project.last_commit_date + ago + - else + No builds yet + %td + - if project.public + %i.fa-globe + Public + - else + %i.fa-lock + Private + %td + = project.commits.count + %td + = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do + %i.fa-remove + Remove diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml new file mode 100644 index 00000000000..73956575a89 --- /dev/null +++ b/app/views/ci/admin/projects/index.html.haml @@ -0,0 +1,14 @@ +%table.table + %thead + %tr + %th ID + %th Name + %th Last build + %th Access + %th Builds + %th + + = render @projects + += paginate @projects + diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml new file mode 100644 index 00000000000..f049b4f4c4e --- /dev/null +++ b/app/views/ci/admin/runner_projects/index.html.haml @@ -0,0 +1,57 @@ +%p.lead + To register new runner visit #{link_to 'this page ', ci_runners_path} + +.row + .col-md-8 + %h5 Activated: + %table.table + %tr + %th Runner ID + %th Runner Description + %th Last build + %th Builds Stats + %th Registered + %th + + - @runner_projects.each do |runner_project| + - runner = runner_project.runner + - builds = runner.builds.where(project_id: @project.id) + %tr + %td + %span.badge.badge-info= runner.id + %td + = runner.display_name + %td + - last_build = builds.last + - if last_build + = link_to last_build.short_sha, [last_build.project, last_build] + - else + unknown + %td + %span.badge.badge-success + #{builds.success.count} + %span / + %span.badge.badge-important + #{builds.failed.count} + %td + #{time_ago_in_words(runner_project.created_at)} ago + %td + = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right' + .col-md-4 + %h5 Available + %table.table + %tr + %th ID + %th Token + %th + + - (Ci::Runner.all - @project.runners).each do |runner| + %tr + %td + = runner.id + %td + = runner.token + %td + = form_for [:ci, @project, @runner_project] do |f| + = f.hidden_field :runner_id, value: runner.id + = f.submit 'Add', class: 'btn btn-sm' diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/ci/admin/runners/_runner.html.haml new file mode 100644 index 00000000000..701782d26bb --- /dev/null +++ b/app/views/ci/admin/runners/_runner.html.haml @@ -0,0 +1,48 @@ +%tr{id: dom_id(runner)} + %td + - if runner.shared? + %span.label.label-success shared + - else + %span.label.label-info specific + - unless runner.active? + %span.label.label-danger paused + + %td + = link_to ci_admin_runner_path(runner) do + = runner.short_sha + %td + .runner-description + = runner.description + %span (#{link_to 'edit', '#', class: 'edit-runner-link'}) + .runner-description-form.hide + = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f| + .form-group + = f.text_field :description, class: 'form-control' + = f.submit 'Save', class: 'btn' + %span (#{link_to 'cancel', '#', class: 'cancel'}) + %td + - if runner.shared? + \- + - else + = runner.projects.count(:all) + %td + #{runner.builds.count(:all)} + %td + - runner.tag_list.each do |tag| + %span.label.label-primary + = tag + %td + - if runner.contacted_at + #{time_ago_in_words(runner.contacted_at)} ago + - else + Never + %td + .pull-right + = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm' +   + - if runner.active? + = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' + - else + = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm' + = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/ci/admin/runners/index.html.haml new file mode 100644 index 00000000000..f1ab3399dcc --- /dev/null +++ b/app/views/ci/admin/runners/index.html.haml @@ -0,0 +1,51 @@ +%p.lead + %span To register 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. + %code #{GitlabCi::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 build + +.append-bottom-20.clearfix + .pull-left + = form_tag ci_admin_runners_path, class: 'form-inline', method: :get do + .form-group + = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token' + = submit_tag 'Search', class: 'btn' + + .pull-right.light + Runners with last contact less than a minute ago: #{@active_runners_cnt} + +%br + +%table.table + %thead + %tr + %th Type + %th Runner token + %th Description + %th Projects + %th Builds + %th Tags + %th Last contact + %th + + = render @runners += paginate @runners diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/ci/admin/runners/show.html.haml new file mode 100644 index 00000000000..0270da53349 --- /dev/null +++ b/app/views/ci/admin/runners/show.html.haml @@ -0,0 +1,118 @@ += content_for :title do + %h3.project-title + Runner ##{@runner.id} + .pull-right + - if @runner.shared? + %span.runner-state.runner-state-shared + Shared + - else + %span.runner-state.runner-state-specific + Specific + + + +- if @runner.shared? + .bs-callout.bs-callout-success + %h4 This runner will process build from ALL UNASSIGNED projects + %p + If you want runners to build only specific projects, enable them in the table below. + Keep in mind that this is a one way transition. +- else + .bs-callout.bs-callout-info + %h4 This runner will process build only from ASSIGNED projects + %p You can't make this a shared runner. +%hr += form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, class: 'form-control' + .help-block You can setup builds to only use runners with specific tags + .form-actions + = f.submit 'Save', class: 'btn btn-save' + +.row + .col-md-6 + %h4 Restrict projects for this runner + - if @runner.projects.any? + %table.table + %thead + %tr + %th Assigned projects + %th + - @runner.runner_projects.each do |runner_project| + - project = runner_project.project + %tr.alert-info + %td + %strong + = project.name + %td + .pull-right + = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + + %table.table + %thead + %tr + %th Project + %th + .pull-right + = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner), + class: 'btn btn-sm assign-all-runner', + title: 'Assign runner to all projects', + method: :put + + %tr + %td + = form_tag ci_admin_runner_path(@runner), class: 'form-inline', method: :get do + .form-group + = search_field_tag :search, params[:search], class: 'form-control' + = submit_tag 'Search', class: 'btn' + + %td + - @projects.each do |project| + %tr + %td + = project.name + %td + .pull-right + = form_for [:ci, :admin, project, project.runner_projects.new] do |f| + = f.hidden_field :runner_id, value: @runner.id + = f.submit 'Enable', class: 'btn btn-xs' + = paginate @projects + + .col-md-6 + %h4 Recent builds served by this runner + %table.builds.runner-builds + %thead + %tr + %th Status + %th Project + %th Commit + %th Finished at + + - @builds.each do |build| + %tr.build.alert{class: build_status_alert_class(build)} + %td.status + = build.status + + %td.status + = build.project.name + + %td.build-link + = link_to ci_project_build_path(build.project, build) do + %strong #{build.short_sha} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/ci/admin/runners/update.js.haml new file mode 100644 index 00000000000..2b7d3067e20 --- /dev/null +++ b/app/views/ci/admin/runners/update.js.haml @@ -0,0 +1,2 @@ +:plain + $("#runner_#{@runner.id}").replaceWith("#{escape_javascript(render(@runner))}") diff --git a/app/views/ci/builds/_build.html.haml b/app/views/ci/builds/_build.html.haml new file mode 100644 index 00000000000..ff9fdbbcb4e --- /dev/null +++ b/app/views/ci/builds/_build.html.haml @@ -0,0 +1,45 @@ +%tr.build.alert{class: build_status_alert_class(build)} + %td.status + = build.status + + %td.build-link + = link_to ci_project_build_path(build.project, build) do + %strong Build ##{build.id} + + %td + = build.stage + + %td + = build.name + .pull-right + - if build.tags.any? + - build.tag_list.each do |tag| + %span.label.label-primary + = tag + - if build.trigger_request + %span.label.label-info triggered + - if build.allow_failure + %span.label.label-danger allowed to fail + + %td.duration + - if build.duration + #{duration_in_words(build.finished_at, build.started_at)} + + %td.timestamp + - if build.finished_at + %span #{time_ago_in_words build.finished_at} ago + + - if build.project.coverage_enabled? + %td.coverage + - if build.coverage + #{build.coverage}% + + %td + - if defined?(controls) && current_user && current_user.has_developer_access?(@project.gitlab_id) + .pull-right + - if build.active? + = link_to cancel_ci_project_build_path(build.project, build, return_to: request.original_url), title: 'Cancel build' do + %i.fa-remove.cred + - elsif build.commands.present? + = link_to retry_ci_project_build_path(build.project, build, return_to: request.original_url), method: :post, title: 'Retry build' do + %i.fa-repeat diff --git a/app/views/ci/builds/show.html.haml b/app/views/ci/builds/show.html.haml new file mode 100644 index 00000000000..fed30847e73 --- /dev/null +++ b/app/views/ci/builds/show.html.haml @@ -0,0 +1,176 @@ +%h4.page-title + = link_to @project.name, @project + @ + = @commit.short_sha + +%p + = link_to ci_project_ref_commit_path(@project, @commit.ref, @commit.sha) do + ← Back to project commit +%hr +#up-build-trace +- if @commit.matrix? + %ul.nav.nav-tabs.append-bottom-10 + - @commit.builds_without_retry_sorted.each do |build| + %li{class: ('active' if build == @build) } + = link_to ci_build_url(build) do + %i{class: build_icon_css_class(build)} + %span + Build ##{build.id} + - if build.name + · + = build.name + + - unless @commit.builds_without_retry.include?(@build) + %li.active + %a + Build ##{@build.id} + · + %i.fa-warning-sign + This build was retried. + +.row + .col-md-9 + .build-head.alert{class: build_status_alert_class(@build)} + %h4 + - if @build.commit.tag? + Build for tag + %code #{@build.ref} + - else + Build for commit + %code #{@build.short_sha} + from + + = link_to ci_project_path(@build.project, ref: @build.ref) do + %span.label.label-primary= "#{@build.ref}" + + - if @build.duration + .pull-right + %span + %i.fa-time + #{duration_in_words(@build.finished_at, @build.started_at)} + + .clearfix + = @build.status + .pull-right + = @build.updated_at.stamp('19:00 Aug 27') + + + + .clearfix + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll + .clearfix + .scroll-controls + = link_to '#up-build-trace', class: 'btn' do + %i.fa-angle-up + = link_to '#down-build-trace', class: 'btn' do + %i.fa-angle-down + + %pre.trace#build-trace + %code.bash + = preserve do + = raw @build.trace_html + %div#down-build-trace + + .col-md-3 + - if @build.coverage + .build-widget + %h4.title + Test coverage + %h1 #{@build.coverage}% + + + .build-widget + %h4.title + Build + - if current_user && current_user.has_developer_access?(@project.gitlab_id) + .pull-right + - if @build.active? + = link_to "Cancel", cancel_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-danger' + - elsif @build.commands.present? + = link_to "Retry", retry_ci_project_build_path(@project, @build), class: 'btn btn-sm btn-primary', method: :post + + - if @build.duration + %p + %span.attr-name Duration: + #{duration_in_words(@build.finished_at, @build.started_at)} + %p + %span.attr-name Created: + #{time_ago_in_words(@build.created_at)} ago + - if @build.finished_at + %p + %span.attr-name Finished: + #{time_ago_in_words(@build.finished_at)} ago + %p + %span.attr-name Runner: + - if @build.runner && current_user && current_user.is_admin + \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)} + - elsif @build.runner + \##{@build.runner.id} + + - if @build.trigger_request + .build-widget + %h4.title + Trigger + + %p + %span.attr-name Token: + #{@build.trigger_request.trigger.short_token} + + - if @build.trigger_request.variables + %p + %span.attr-name Variables: + + %code + - @build.trigger_request.variables.each do |key, value| + #{key}=#{value} + + .build-widget + %h4.title + Commit + .pull-right + %small #{build_commit_link @build} + + - if @build.commit.compare? + %p + %span.attr-name Compare: + #{build_compare_link @build} + %p + %span.attr-name Branch: + #{build_ref_link @build} + %p + %span.attr-name Author: + #{@build.commit.git_author_name} + %p + %span.attr-name Message: + #{@build.commit.git_commit_message} + + - if @build.tags.any? + .build-widget + %h4.title + Tags + - @build.tag_list.each do |tag| + %span.label.label-primary + = tag + + - if @builds.present? + .build-widget + %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}: + %table.builds + - @builds.each_with_index do |build, i| + %tr.build.alert{class: build_status_alert_class(build)} + %td + = link_to ci_build_url(build) do + %span ##{build.id} + %td + - if build.name + = build.name + %td.status= build.status + + + = paginate @builds + + +:javascript + new CiBuild("#{ci_build_url(@build)}", "#{@build.status}") diff --git a/app/views/ci/charts/_build_times.haml b/app/views/ci/charts/_build_times.haml new file mode 100644 index 00000000000..c3c2f572414 --- /dev/null +++ b/app/views/ci/charts/_build_times.haml @@ -0,0 +1,21 @@ +%fieldset + %legend + Commit duration in minutes for last 30 commits + + %canvas#build_timesChart.padded{width: 800, height: 300} + +:javascript + var data = { + labels : #{@charts[:build_times].labels.to_json}, + datasets : [ + { + fillColor : "#4A3", + strokeColor : "rgba(151,187,205,1)", + pointColor : "rgba(151,187,205,1)", + pointStrokeColor : "#fff", + data : #{@charts[:build_times].build_times.to_json} + } + ] + } + var ctx = $("#build_timesChart").get(0).getContext("2d"); + new Chart(ctx).Line(data,{"scaleOverlay": true}); diff --git a/app/views/ci/charts/_builds.haml b/app/views/ci/charts/_builds.haml new file mode 100644 index 00000000000..1b0039fb834 --- /dev/null +++ b/app/views/ci/charts/_builds.haml @@ -0,0 +1,41 @@ +%fieldset + %legend + Builds chart for last week + (#{date_from_to(Date.today - 7.days, Date.today)}) + + %canvas#weekChart.padded{width: 800, height: 200} + +%fieldset + %legend + Builds chart for last month + (#{date_from_to(Date.today - 30.days, Date.today)}) + + %canvas#monthChart.padded{width: 800, height: 300} + +%fieldset + %legend Builds chart for last year + %canvas#yearChart.padded{width: 800, height: 400} + +- [:week, :month, :year].each do |scope| + :javascript + var data = { + labels : #{@charts[scope].labels.to_json}, + datasets : [ + { + fillColor : "rgba(220,220,220,0.5)", + strokeColor : "rgba(220,220,220,1)", + pointColor : "rgba(220,220,220,1)", + pointStrokeColor : "#EEE", + data : #{@charts[scope].total.to_json} + }, + { + fillColor : "#4A3", + strokeColor : "rgba(151,187,205,1)", + pointColor : "rgba(151,187,205,1)", + pointStrokeColor : "#fff", + data : #{@charts[scope].success.to_json} + } + ] + } + var ctx = $("##{scope}Chart").get(0).getContext("2d"); + new Chart(ctx).Line(data,{"scaleOverlay": true}); diff --git a/app/views/ci/charts/_overall.haml b/app/views/ci/charts/_overall.haml new file mode 100644 index 00000000000..f522f35a629 --- /dev/null +++ b/app/views/ci/charts/_overall.haml @@ -0,0 +1,21 @@ +%fieldset + %legend Overall + %p + Total: + %strong= pluralize @project.builds.count(:all), 'build' + %p + Successful: + %strong= pluralize @project.builds.success.count(:all), 'build' + %p + Failed: + %strong= pluralize @project.builds.failed.count(:all), 'build' + + %p + Success ratio: + %strong + #{success_ratio(@project.builds.success, @project.builds.failed)}% + + %p + Commits covered: + %strong + = @project.commits.count(:all) diff --git a/app/views/ci/charts/show.html.haml b/app/views/ci/charts/show.html.haml new file mode 100644 index 00000000000..b5fcfc1563c --- /dev/null +++ b/app/views/ci/charts/show.html.haml @@ -0,0 +1,4 @@ +#charts + = render 'builds' + = render 'build_times' += render 'overall' diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml new file mode 100644 index 00000000000..a955a5b6479 --- /dev/null +++ b/app/views/ci/commits/_commit.html.haml @@ -0,0 +1,32 @@ +%tr.build.alert{class: commit_status_alert_class(commit)} + %td.status + = commit.status + - if commit.running? + · + = commit.stage + + + %td.build-link + = link_to ci_project_ref_commit_path(commit.project, commit.ref, commit.sha) do + %strong #{commit.short_sha} + + %td.build-message + %span= truncate_first_line(commit.git_commit_message) + + %td.build-branch + - unless @ref + %span + = link_to truncate(commit.ref, length: 25), ci_project_path(@project, ref: commit.ref) + + %td.duration + - if commit.duration > 0 + #{time_interval_in_words commit.duration} + + %td.timestamp + - if commit.finished_at + %span #{time_ago_in_words commit.finished_at} ago + + - if commit.project.coverage_enabled? + %td.coverage + - if commit.coverage + #{commit.coverage}% diff --git a/app/views/ci/commits/show.html.haml b/app/views/ci/commits/show.html.haml new file mode 100644 index 00000000000..4cf567c77e6 --- /dev/null +++ b/app/views/ci/commits/show.html.haml @@ -0,0 +1,96 @@ +%h4.page-title + = @project.name + @ + #{gitlab_commit_link(@project, @commit.sha)} +%p + = link_to ci_project_path(@project) do + ← Back to project commits +%hr +.commit-info + %pre.commit-message + #{@commit.git_commit_message} + + .row + .col-sm-6 + - if @commit.compare? + %p + %span.attr-name Compare: + #{gitlab_compare_link(@project, @commit.short_before_sha, @commit.short_sha)} + - else + %p + %span.attr-name Commit: + #{gitlab_commit_link(@project, @commit.sha)} + + %p + %span.attr-name Branch: + #{gitlab_ref_link(@project, @commit.ref)} + .col-sm-6 + %p + %span.attr-name Author: + #{@commit.git_author_name} (#{@commit.git_author_email}) + - if @commit.created_at + %p + %span.attr-name Created at: + #{@commit.created_at.to_s(:short)} + +- if current_user && current_user.has_developer_access?(@project.gitlab_id) + .pull-right + - if @commit.builds.running_or_pending.any? + = link_to "Cancel", cancel_ci_project_ref_commit_path(@project, @commit.ref, @commit.sha), class: 'btn btn-sm btn-danger' + + +- if @commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @commit.yaml_errors.split(",").each do |error| + %li= error + +- unless @commit.push_data[:ci_yaml_file] + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +%h3 Status + +.build.alert{class: commit_status_alert_class(@commit)} + .status + = @commit.status.titleize + +%h3 + Builds + - if @commit.duration > 0 + %small.pull-right + %i.fa-time + #{time_interval_in_words @commit.duration} + +%table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + %th + = render @commit.builds_without_retry_sorted, controls: true + +- if @commit.retried_builds.any? + %h3 + Retried builds + + %table.builds + %thead + %tr + %th Status + %th Build ID + %th Stage + %th Name + %th Duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + %th + = render @commit.retried_builds diff --git a/app/views/ci/errors/show.haml b/app/views/ci/errors/show.haml new file mode 100644 index 00000000000..2788112c835 --- /dev/null +++ b/app/views/ci/errors/show.haml @@ -0,0 +1,2 @@ +%h3.error Error += @error diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml new file mode 100644 index 00000000000..779f49b3d3a --- /dev/null +++ b/app/views/ci/events/index.html.haml @@ -0,0 +1,19 @@ +%h3.page-title Events + +%table.table + %thead + %tr + %th User ID + %th Description + %th When + - @events.each do |event| + %tr + %td + = event.user_id + %td + = event.description + %td.light + = time_ago_in_words event.updated_at + ago + += paginate @events \ No newline at end of file diff --git a/app/views/ci/helps/oauth2.html.haml b/app/views/ci/helps/oauth2.html.haml new file mode 100644 index 00000000000..2031b7340d4 --- /dev/null +++ b/app/views/ci/helps/oauth2.html.haml @@ -0,0 +1,20 @@ +.welcome-block + %h1 + Welcome to GitLab CI + %p + GitLab CI integrates with your GitLab installation and runs tests for your projects. + + %h3 You need only 2 steps to set it up + + %ol + %li + In the GitLab admin area under OAuth applications create a new entry. The redirect url should be + %code= callback_ci_user_sessions_url + %li + Update the GitLab CI config with the application id and the application secret from GitLab. + %li + Restart your GitLab CI instance + %li + Refresh this page when GitLab CI has started again + + diff --git a/app/views/ci/helps/show.html.haml b/app/views/ci/helps/show.html.haml new file mode 100644 index 00000000000..5acdf9fa98a --- /dev/null +++ b/app/views/ci/helps/show.html.haml @@ -0,0 +1,40 @@ +.jumbotron + %h2 + GitLab CI + %span= GitlabCi::VERSION + %small= GitlabCi::REVISION + %p + GitLab CI integrates with your GitLab installation and run tests for your projects. + %br + Login with your GitLab account, add a project with one click and enjoy running your tests. + %br + Read more about GitLab CI at #{link_to "about.gitlab.com/gitlab-ci", "https://about.gitlab.com/gitlab-ci/", target: "_blank"}. + + +.bs-callout.bs-callout-success + %h4 + = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/api' do + %i.fa-cogs + API + %p Explore how you can access GitLab CI via the API. + +.bs-callout.bs-callout-info + %h4 + = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/tree/master/doc/examples' do + %i.fa-info-sign + Build script examples + %p This includes the build script we use to test GitLab CE. + +.bs-callout.bs-callout-danger + %h4 + = link_to 'https://gitlab.com/gitlab-org/gitlab-ci/issues' do + %i.fa-bug + Issue tracker + %p Reports about recent bugs and problems.. + +.bs-callout.bs-callout-warning + %h4 + = link_to 'http://feedback.gitlab.com/forums/176466-general/category/64310-gitlab-ci' do + %i.fa-thumbs-up + Feedback forum + %p Suggest improvements or new features for GitLab CI. diff --git a/app/views/ci/kaminari/_first_page.html.haml b/app/views/ci/kaminari/_first_page.html.haml new file mode 100644 index 00000000000..a1bbf18690c --- /dev/null +++ b/app/views/ci/kaminari/_first_page.html.haml @@ -0,0 +1,2 @@ +%li + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/app/views/ci/kaminari/_gap.html.haml b/app/views/ci/kaminari/_gap.html.haml new file mode 100644 index 00000000000..dfe33aac21d --- /dev/null +++ b/app/views/ci/kaminari/_gap.html.haml @@ -0,0 +1,2 @@ +%li.disabled + = link_to raw(t 'views.pagination.truncate'), '#' diff --git a/app/views/ci/kaminari/_last_page.html.haml b/app/views/ci/kaminari/_last_page.html.haml new file mode 100644 index 00000000000..e70697d04ad --- /dev/null +++ b/app/views/ci/kaminari/_last_page.html.haml @@ -0,0 +1,2 @@ +%li + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/app/views/ci/kaminari/_next_page.html.haml b/app/views/ci/kaminari/_next_page.html.haml new file mode 100644 index 00000000000..ea9af4539e0 --- /dev/null +++ b/app/views/ci/kaminari/_next_page.html.haml @@ -0,0 +1,2 @@ +%li + = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote diff --git a/app/views/ci/kaminari/_page.html.haml b/app/views/ci/kaminari/_page.html.haml new file mode 100644 index 00000000000..9df7ce02f8f --- /dev/null +++ b/app/views/ci/kaminari/_page.html.haml @@ -0,0 +1,2 @@ +%li{class: "#{'active' if page.current?}"} + = link_to page, page.current? ? '#' : url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} diff --git a/app/views/ci/kaminari/_paginator.html.haml b/app/views/ci/kaminari/_paginator.html.haml new file mode 100644 index 00000000000..07fdb1e08a6 --- /dev/null +++ b/app/views/ci/kaminari/_paginator.html.haml @@ -0,0 +1,11 @@ += paginator.render do + %ul.pagination + = first_page_tag unless current_page.first? + = prev_page_tag unless current_page.first? + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag unless current_page.last? + = last_page_tag unless current_page.last? diff --git a/app/views/ci/kaminari/_prev_page.html.haml b/app/views/ci/kaminari/_prev_page.html.haml new file mode 100644 index 00000000000..dab3b318dac --- /dev/null +++ b/app/views/ci/kaminari/_prev_page.html.haml @@ -0,0 +1,2 @@ +%li + = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml new file mode 100644 index 00000000000..903b92de689 --- /dev/null +++ b/app/views/ci/lints/_create.html.haml @@ -0,0 +1,39 @@ +- if @status + %p + %b Status: + syntax is correct + %i.fa-ok.correct-syntax + + %table.table.table-bordered + %thead + %tr + %th Parameter + %th Value + %tbody + - @stages.each do |stage| + - @builds.select { |build| build[:stage] == stage }.each do |build| + %tr + %td #{stage.capitalize} Job - #{build[:name]} + %td + %pre + = simple_format build[:script] + + %br + %b Tag list: + = build[:tags] + %br + %b Refs only: + = build[:only] && build[:only].join(", ") + %br + %b Refs except: + = build[:except] && build[:except].join(", ") + +-else + %p + %b Status: + syntax is incorrect + %i.fa-remove.incorrect-syntax + %b Error: + = @error + + diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml new file mode 100644 index 00000000000..a96c0b11b6e --- /dev/null +++ b/app/views/ci/lints/create.js.haml @@ -0,0 +1,2 @@ +:plain + $(".results").html("#{escape_javascript(render "create")}") \ No newline at end of file diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml new file mode 100644 index 00000000000..b0fd5dd8e58 --- /dev/null +++ b/app/views/ci/lints/show.html.haml @@ -0,0 +1,25 @@ +%h2 Check your .gitlab-ci.yml +%hr + += form_tag ci_lint_path, method: :post, remote: true do + .control-group + = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label' + .controls + = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true + + .control-group.clearfix + .controls.pull-left.prepend-top-10 + = submit_tag "Validate", class: 'btn btn-success submit-yml' + + +%p.text-center.loading + %i.fa-refresh.fa-spin + +.results.prepend-top-20 + +:coffeescript + $(".loading").hide() + $('form').bind 'ajax:beforeSend', -> + $(".loading").show() + $('form').bind 'ajax:complete', -> + $(".loading").hide() diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/ci/notify/build_fail_email.html.haml new file mode 100644 index 00000000000..d818e8b6756 --- /dev/null +++ b/app/views/ci/notify/build_fail_email.html.haml @@ -0,0 +1,19 @@ +- content_for :header do + %h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + GitLab CI (build failed) +%h3 + Project: + = link_to ci_project_url(@project) do + = @project.name + +%p + Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} +%p + Author: #{@build.commit.git_author_name} +%p + Branch: #{@build.commit.ref} +%p + Message: #{@build.commit.git_commit_message} + +%p + Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/ci/notify/build_fail_email.text.erb new file mode 100644 index 00000000000..1add215a1c8 --- /dev/null +++ b/app/views/ci/notify/build_fail_email.text.erb @@ -0,0 +1,9 @@ +Build failed for <%= @project.name %> + +Status: <%= @build.status %> +Commit: <%= @build.commit.short_sha %> +Author: <%= @build.commit.git_author_name %> +Branch: <%= @build.commit.ref %> +Message: <%= @build.commit.git_commit_message %> + +Url: <%= ci_project_build_url(@build.project, @build) %> diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/ci/notify/build_success_email.html.haml new file mode 100644 index 00000000000..a20dcaee24e --- /dev/null +++ b/app/views/ci/notify/build_success_email.html.haml @@ -0,0 +1,20 @@ +- content_for :header do + %h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"} + GitLab CI (build successful) + +%h3 + Project: + = link_to ci_project_url(@project) do + = @project.name + +%p + Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)} +%p + Author: #{@build.commit.git_author_name} +%p + Branch: #{@build.commit.ref} +%p + Message: #{@build.commit.git_commit_message} + +%p + Url: #{link_to @build.short_sha, ci_project_build_url(@project, @build)} diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/ci/notify/build_success_email.text.erb new file mode 100644 index 00000000000..7ebd17e7270 --- /dev/null +++ b/app/views/ci/notify/build_success_email.text.erb @@ -0,0 +1,9 @@ +Build successful for <%= @project.name %> + +Status: <%= @build.status %> +Commit: <%= @build.commit.short_sha %> +Author: <%= @build.commit.git_author_name %> +Branch: <%= @build.commit.ref %> +Message: <%= @build.commit.git_commit_message %> + +Url: <%= ci_project_build_url(@build.project, @build) %> diff --git a/app/views/ci/projects/_form.html.haml b/app/views/ci/projects/_form.html.haml new file mode 100644 index 00000000000..d50e1a83b06 --- /dev/null +++ b/app/views/ci/projects/_form.html.haml @@ -0,0 +1,101 @@ +.bs-callout.help-callout + %p + If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path} + %p + Edit your + #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@project)} + += nested_form_for [:ci, @project], html: { class: 'form-horizontal' } do |f| + - if @project.errors.any? + #error_explanation + %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" + .alert.alert-error + %ul + - @project.errors.full_messages.each do |msg| + %li= msg + + %fieldset + %legend Build settings + .form-group + = label_tag nil, class: 'control-label' do + Get code + .col-sm-10 + %p Get recent application code using the following command: + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'false' + %strong git clone + .light Slower but makes sure you have a clean dir before every build + .radio + = label_tag do + = f.radio_button :allow_git_fetch, 'true' + %strong git fetch + .light Faster + .form-group + = f.label :timeout_in_minutes, 'Timeout', class: 'control-label' + .col-sm-10 + = f.number_field :timeout_in_minutes, class: 'form-control', min: '0' + .light per build in minutes + + + %fieldset + %legend Build Schedule + .form-group + = f.label :always_build, 'Schedule build', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :always_build do + = f.check_box :always_build + %span.light Repeat last build after X hours if no builds + .form-group + = f.label :polling_interval, "Build interval", class: 'control-label' + .col-sm-10 + = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control' + .light In hours + + %fieldset + %legend Project settings + .form-group + = f.label :default_ref, "Make tabs for the following branches", class: 'control-label' + .col-sm-10 + = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable' + .light You will be able to filter builds by the following branches + .form-group + = f.label :public, 'Public mode', class: 'control-label' + .col-sm-10 + .checkbox + = f.label :public do + = f.check_box :public + %span.light Anyone can see project and builds + .form-group + = f.label :coverage_regex, "Test coverage parsing", class: 'control-label' + .col-sm-10 + .input-group + %span.input-group-addon / + = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + .light We will use this regular expression to find test coverage output in build trace. Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%$ + + + + %fieldset + %legend Advanced settings + .form-group + = f.label :token, "CI token", class: 'control-label' + .col-sm-10 + = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89' + + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' + = link_to 'Cancel', projects_path, class: 'btn' + - unless @project.new_record? + = link_to 'Remove Project', ci_project_path(@project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right' diff --git a/app/views/ci/projects/_gl_projects.html.haml b/app/views/ci/projects/_gl_projects.html.haml new file mode 100644 index 00000000000..6ed19e13887 --- /dev/null +++ b/app/views/ci/projects/_gl_projects.html.haml @@ -0,0 +1,15 @@ +- @gl_projects.sort_by(&:name_with_namespace).each do |project| + %tr.light + %td + = project.name_with_namespace + %td + %small Not added to CI + %td + %td + - if Ci::Project.already_added?(project) + %strong.cgreen + Added + - else + = form_tag ci_projects_path do + = hidden_field_tag :project, project.to_h.to_json + = submit_tag 'Add project to CI', class: 'btn btn-default btn-sm' diff --git a/app/views/ci/projects/_info.html.haml b/app/views/ci/projects/_info.html.haml new file mode 100644 index 00000000000..1888e1bde93 --- /dev/null +++ b/app/views/ci/projects/_info.html.haml @@ -0,0 +1,2 @@ +- if no_runners_for_project?(@project) + = render 'no_runners' diff --git a/app/views/ci/projects/_no_runners.html.haml b/app/views/ci/projects/_no_runners.html.haml new file mode 100644 index 00000000000..c0a296fb17d --- /dev/null +++ b/app/views/ci/projects/_no_runners.html.haml @@ -0,0 +1,8 @@ +.alert.alert-danger + %p + There are NO runners to build this project. + %br + You can add Specific runner for this project on Runners page + + - if current_user.is_admin + or add Shared runner for whole application in admin are. diff --git a/app/views/ci/projects/_project.html.haml b/app/views/ci/projects/_project.html.haml new file mode 100644 index 00000000000..3e893410df8 --- /dev/null +++ b/app/views/ci/projects/_project.html.haml @@ -0,0 +1,22 @@ +- last_commit = project.last_commit +%tr.alert{class: commit_status_alert_class(last_commit) } + %td + = link_to project do + %strong= project.name + %td + - if last_commit + #{last_commit.status} (#{commit_link(last_commit)}) + - if project.last_commit_date + = time_ago_in_words project.last_commit_date + ago + - else + No builds yet + %td + - if project.public + %i.fa-globe + Public + - else + %i.fa-lock + Private + %td + = project.commits.count diff --git a/app/views/ci/projects/_public.html.haml b/app/views/ci/projects/_public.html.haml new file mode 100644 index 00000000000..c2157ab741a --- /dev/null +++ b/app/views/ci/projects/_public.html.haml @@ -0,0 +1,21 @@ += content_for :title do + %h3.project-title + Public projects + +.bs-callout + = link_to new_ci_user_sessions_path(state: generate_oauth_state(request.fullpath)) do + %strong Login with GitLab + to see your private projects + +- if @projects.present? + .projects + %table.table + %tr + %th Name + %th Last commit + %th Access + %th Commits + = render @projects + = paginate @projects +- else + %h4 No public projects yet diff --git a/app/views/ci/projects/_search.html.haml b/app/views/ci/projects/_search.html.haml new file mode 100644 index 00000000000..37fb804d8d0 --- /dev/null +++ b/app/views/ci/projects/_search.html.haml @@ -0,0 +1,18 @@ +.search + = form_tag "#", method: :get, class: 'navbar-form' do |f| + .form-group + .input-group + = search_field_tag "search", params[:search], placeholder: "Search", class: "search-input form-control" + .input-group-addon + %i.fa-search + + +:coffeescript + $('.search .navbar-form').submit -> + NProgress.start() + query = $('.search .navbar-form .search-input').val() + $.get '#{gitlab_ci_projects_path}', { search: query }, (data) -> + $(".projects").html data.html + NProgress.done() + CiPager.init "#{gitlab_ci_projects_path}" + "?search=" + query, #{Ci::ProjectsController::PROJECTS_BATCH}, false + false diff --git a/app/views/ci/projects/edit.html.haml b/app/views/ci/projects/edit.html.haml new file mode 100644 index 00000000000..298007a6565 --- /dev/null +++ b/app/views/ci/projects/edit.html.haml @@ -0,0 +1,21 @@ +- if @project.generated_yaml_config + %p.alert.alert-danger + CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_project_path(@project)} + or + %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview + yaml file which is based on your old jobs. + Put this file to the root of your project and name it .gitlab-ci.yml + += render 'form' + +- if @project.generated_yaml_config + #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"} + .modal-dialog + .modal-content + .modal-header + %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} × + %h4.modal-title Content of .gitlab-ci.yml + .modal-body + = text_area_tag :yaml, @project.generated_yaml_config, size: "70x25", class: "form-control" + .modal-footer + %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close diff --git a/app/views/ci/projects/gitlab.html.haml b/app/views/ci/projects/gitlab.html.haml new file mode 100644 index 00000000000..dbc0ea0880f --- /dev/null +++ b/app/views/ci/projects/gitlab.html.haml @@ -0,0 +1,35 @@ +- if @offset == 0 + .clearfix.light + .pull-left.fetch-status + Fetched from GitLab (#{link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink}) + - if params[:search].present? + by keyword: "#{params[:search]}", + #{time_ago_in_words(current_user.sync_at)} ago. + = link_to gitlab_ci_projects_path(reset_cache: true, search: params[:search]), class: 'sync-now btn btn-sm btn-default reset-cache' do + %i.fa-refresh + Sync now + %br + + .pull-right + #{@total_count} projects, #{@projects.size} of them added to CI + %br + + %table.table.projects-table.content-list + %thead + %tr + %th Project Name + %th Last commit + %th Access + %th Commits + + = render @projects + + = render "gl_projects" + + %p.text-center.hide.loading + %i.fa-refresh.fa-spin + +- else + = render @projects + + = render "gl_projects" diff --git a/app/views/ci/projects/index.html.haml b/app/views/ci/projects/index.html.haml new file mode 100644 index 00000000000..6243a28f9e2 --- /dev/null +++ b/app/views/ci/projects/index.html.haml @@ -0,0 +1,22 @@ +- if current_user + = content_for :title do + %h3.project-title + Dashboard + .pull-right + = render "search" + + .projects + %p.fetch-status.light + %i.fa-refresh.fa-spin + Please wait while we fetch from GitLab (#{GitlabCi.config.gitlab_server.url}) + :coffeescript + $.get '#{gitlab_ci_projects_path}', (data) -> + $(".projects").html data.html + $('.projects').on 'click', '.reset-cache', -> + $.get '#{gitlab_ci_projects_path}', { reset_cache: true }, (data) -> + $(".projects").html data.html + false + CiPager.init "#{gitlab_ci_projects_path}", #{Ci::ProjectsController::PROJECTS_BATCH}, false + +- else + = render 'public' diff --git a/app/views/ci/projects/show.html.haml b/app/views/ci/projects/show.html.haml new file mode 100644 index 00000000000..27899591391 --- /dev/null +++ b/app/views/ci/projects/show.html.haml @@ -0,0 +1,59 @@ += render 'ci/shared/guide' unless @project.setup_finished? + +- if current_user && current_user.can_manage_project?(@project.gitlab_id) && !@project.any_runners? + .alert.alert-danger + Builds for this project wont be served unless you configure runners on + = link_to "Runners page", ci_project_runners_path(@project) + +%ul.nav.nav-tabs.append-bottom-20 + %li{class: ref_tab_class} + = link_to 'All commits', ci_project_path(@project) + - @project.tracked_refs.each do |ref| + %li{class: ref_tab_class(ref)} + = link_to ref, ci_project_path(@project, ref: ref) + + - if @ref && !@project.tracked_refs.include?(@ref) + %li{class: 'active'} + = link_to @ref, ci_project_path(@project, ref: @ref) + + + +- if @ref + %p + Paste build status image for #{@ref} with next link + = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do + Status Badge + .badge-codes-block.bs-callout.bs-callout-info.hide + %p + Status badge for + %span.label.label-info #{@ref} + branch + %div + %label Markdown: + = text_field_tag 'badge_md', markdown_badge_code(@project, @ref), readonly: true, class: 'form-control' + %label Html: + = text_field_tag 'badge_html', html_badge_code(@project, @ref), readonly: true, class: 'form-control' + + + + +%table.builds + %thead + %tr + %th Status + %th Commit + %th Message + %th Branch + %th Total duration + %th Finished at + - if @project.coverage_enabled? + %th Coverage + + = render @commits + += paginate @commits + +- if @commits.empty? + .bs-callout + %h4 No commits yet + diff --git a/app/views/ci/runners/_runner.html.haml b/app/views/ci/runners/_runner.html.haml new file mode 100644 index 00000000000..7ead5736bb1 --- /dev/null +++ b/app/views/ci/runners/_runner.html.haml @@ -0,0 +1,35 @@ +%li.runner{id: dom_id(runner)} + %h4 + = runner_status_icon(runner) + %span.monospace + - if @runners.include?(runner) + = link_to runner.short_sha, [:ci, @project, runner] + %small + =link_to edit_ci_project_runner_path(@project, runner) do + %i.fa.fa-edit.btn + - else + = runner.short_sha + + .pull-right + - if @runners.include?(runner) + - if runner.belongs_to_one_project? + = link_to 'Remove runner', [:ci, @project, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + - else + - runner_project = @project.runner_projects.find_by(runner_id: runner) + = link_to 'Disable for this project', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' + - elsif runner.specific? + = form_for [:ci, @project, @project.runner_projects.new] do |f| + = f.hidden_field :runner_id, value: runner.id + = f.submit 'Enable for this project', class: 'btn btn-sm' + .pull-right + %small.light + \##{runner.id} + - if runner.description.present? + %p.runner-description + = runner.description + - if runner.tag_list.present? + %p + - runner.tag_list.each do |tag| + %span.label.label-primary + = tag + diff --git a/app/views/ci/runners/_shared_runners.html.haml b/app/views/ci/runners/_shared_runners.html.haml new file mode 100644 index 00000000000..944b3fd930d --- /dev/null +++ b/app/views/ci/runners/_shared_runners.html.haml @@ -0,0 +1,23 @@ +%h3 Shared runners + +.bs-callout.bs-callout-warning + GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X. + %hr + - if @project.shared_runners_enabled + = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-warning', method: :post do + Disable shared runners + - else + = link_to toggle_shared_runners_ci_project_path(@project), class: 'btn btn-success', method: :post do + Enable shared runners +   for this project + +- if @shared_runners_count.zero? + This application has no shared runners yet. + Please use specific runners or ask administrator to create one +- else + %h4.underlined-title Available shared runners - #{@shared_runners_count} + %ul.bordered-list.available-shared-runners + = render @shared_runners.first(10) + - if @shared_runners_count > 10 + .light + and #{@shared_runners_count - 10} more... diff --git a/app/views/ci/runners/_specific_runners.html.haml b/app/views/ci/runners/_specific_runners.html.haml new file mode 100644 index 00000000000..0604e7a46c5 --- /dev/null +++ b/app/views/ci/runners/_specific_runners.html.haml @@ -0,0 +1,29 @@ +%h3 Specific runners + +.bs-callout.help-callout + %h4 How to setup a new project specific runner + + %ol + %li + Install GitLab Runner software. + Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it + %li + Specify following URL during runner setup: + %code #{ci_root_url(only_path: false)} + %li + Use the following registration token during setup: + %code #{@project.token} + %li + Start runner! + + +- if @runners.any? + %h4.underlined-title Runners activated for this project + %ul.bordered-list.activated-specific-runners + = render @runners + +- if @specific_runners.any? + %h4.underlined-title Available specific runners + %ul.bordered-list.available-specific-runners + = render @specific_runners + = paginate @specific_runners diff --git a/app/views/ci/runners/edit.html.haml b/app/views/ci/runners/edit.html.haml new file mode 100644 index 00000000000..81c8e58ae2b --- /dev/null +++ b/app/views/ci/runners/edit.html.haml @@ -0,0 +1,27 @@ +%h4 Runner ##{@runner.id} +%hr += form_for [:ci, @project, @runner], html: { class: 'form-horizontal' } do |f| + .form-group + = label :active, "Active", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :active + %span.light Paused runners don't accept new builds + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, class: 'form-control' + .help-block You can setup jobs to only use runners with specific tags + .form-actions + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/ci/runners/index.html.haml b/app/views/ci/runners/index.html.haml new file mode 100644 index 00000000000..529fb9c296d --- /dev/null +++ b/app/views/ci/runners/index.html.haml @@ -0,0 +1,25 @@ +.light + %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. + + %p Each runner can be in one of the following states: + %div + %ul + %li + %span.label.label-success active + \- runner is active and can process any new build + %li + %span.label.label-danger paused + \- runner is paused and will not receive any new build + +%hr + +%p.lead To start serving your builds you can either add specific runners to your project or use shared runners +.row + .col-sm-6 + = render 'specific_runners' + .col-sm-6 + = render 'shared_runners' diff --git a/app/views/ci/runners/show.html.haml b/app/views/ci/runners/show.html.haml new file mode 100644 index 00000000000..ffec495f85a --- /dev/null +++ b/app/views/ci/runners/show.html.haml @@ -0,0 +1,64 @@ += content_for :title do + %h3.project-title + Runner ##{@runner.id} + .pull-right + - if @runner.shared? + %span.runner-state.runner-state-shared + Shared + - else + %span.runner-state.runner-state-specific + Specific + +%table.table + %thead + %tr + %th Property Name + %th Value + %tr + %td + Tags + %td + - @runner.tag_list.each do |tag| + %span.label.label-primary + = tag + %tr + %td + Name + %td + = @runner.name + %tr + %td + Version + %td + = @runner.version + %tr + %td + Revision + %td + = @runner.revision + %tr + %td + Platform + %td + = @runner.platform + %tr + %td + Architecture + %td + = @runner.architecture + %tr + %td + Description + %td + = @runner.description + %tr + %td + Last contact + %td + - if @runner.contacted_at + #{time_ago_in_words(@runner.contacted_at)} ago + - else + Never + + + diff --git a/app/views/ci/services/_form.html.haml b/app/views/ci/services/_form.html.haml new file mode 100644 index 00000000000..9110aaa0528 --- /dev/null +++ b/app/views/ci/services/_form.html.haml @@ -0,0 +1,57 @@ +%h3.page-title + = @service.title + = boolean_to_icon @service.activated? + +%p= @service.description + +.back-link + = link_to ci_project_services_path(@project) do + ← to services + +%hr + += form_for(@service, as: :service, url: ci_project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| + - if @service.errors.any? + .alert.alert-danger + %ul + - @service.errors.full_messages.each do |msg| + %li= msg + + - if @service.help.present? + .bs-callout + = @service.help + + .form-group + = f.label :active, "Active", class: "control-label" + .col-sm-10 + = f.check_box :active + + - @service.fields.each do |field| + - name = field[:name] + - label = field[:label] || name + - value = @service.send(name) + - type = field[:type] + - placeholder = field[:placeholder] + - choices = field[:choices] + - default_choice = field[:default_choice] + - help = field[:help] + + .form-group + = f.label label, class: "control-label" + .col-sm-10 + - if type == 'text' + = f.text_field name, class: "form-control", placeholder: placeholder + - elsif type == 'textarea' + = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder + - elsif type == 'checkbox' + = f.check_box name + - elsif type == 'select' + = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } + - if help + .light #{help} + + .form-actions + = f.submit 'Save', class: 'btn btn-save' +   + - if @service.valid? && @service.activated? && @service.can_test? + = link_to 'Test settings', test_ci_project_service_path(@project, @service.to_param), class: 'btn' diff --git a/app/views/ci/services/edit.html.haml b/app/views/ci/services/edit.html.haml new file mode 100644 index 00000000000..bcc5832792f --- /dev/null +++ b/app/views/ci/services/edit.html.haml @@ -0,0 +1 @@ += render 'form' diff --git a/app/views/ci/services/index.html.haml b/app/views/ci/services/index.html.haml new file mode 100644 index 00000000000..37e5723b541 --- /dev/null +++ b/app/views/ci/services/index.html.haml @@ -0,0 +1,22 @@ +%h3.page-title Project services +%p.light Project services allow you to integrate GitLab CI with other applications + +%table.table + %thead + %tr + %th + %th Service + %th Desription + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = boolean_to_icon service.activated? + %td + = link_to edit_ci_project_service_path(@project, service.to_param) do + %strong= service.title + %td + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml new file mode 100644 index 00000000000..8a42f29b77c --- /dev/null +++ b/app/views/ci/shared/_guide.html.haml @@ -0,0 +1,15 @@ +.bs-callout.help-callout + %h4 How to setup CI for this project + + %ol + %li + Add at least one runner to the project. + Go to #{link_to 'Runners page', ci_project_runners_path(@project), target: :blank} for instructions. + %li + Put the .gitlab-ci.yml in the root of your repository. Examples can be found in #{link_to "Configuring project (.gitlab-ci.yml)", "http://doc.gitlab.com/ci/yaml/README.html", target: :blank}. + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + %li + Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank} + and press the "Test settings" button. + %li + Return to this page and refresh it, it should show a new build. diff --git a/app/views/ci/shared/_no_runners.html.haml b/app/views/ci/shared/_no_runners.html.haml new file mode 100644 index 00000000000..f56c37d9b37 --- /dev/null +++ b/app/views/ci/shared/_no_runners.html.haml @@ -0,0 +1,7 @@ +.alert.alert-danger + %p + Now you need Runners to process your builds. + %span + Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it + + diff --git a/app/views/ci/triggers/_trigger.html.haml b/app/views/ci/triggers/_trigger.html.haml new file mode 100644 index 00000000000..addfbfcb0d4 --- /dev/null +++ b/app/views/ci/triggers/_trigger.html.haml @@ -0,0 +1,14 @@ +%tr + %td + .clearfix + %span.monospace= trigger.token + + %td + - if trigger.last_trigger_request + #{time_ago_in_words(trigger.last_trigger_request.created_at)} ago + - else + Never + + %td + .pull-right + = link_to 'Revoke', ci_project_trigger_path(@project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped" diff --git a/app/views/ci/triggers/index.html.haml b/app/views/ci/triggers/index.html.haml new file mode 100644 index 00000000000..f04c116231d --- /dev/null +++ b/app/views/ci/triggers/index.html.haml @@ -0,0 +1,67 @@ +%h3 + Triggers + +%p.light + Triggers can be used to force a rebuild of a specific branch or tag with an API call. + +%hr.clearfix + +-if @triggers.any? + %table.table + %thead + %th Token + %th Last used + %th + = render @triggers +- else + %h4 No triggers + += form_for [:ci, @project, @trigger], html: { class: 'form-horizontal' } do |f| + .clearfix + = f.submit "Add Trigger", class: 'btn btn-success pull-right' + +%hr.clearfix + +-if @triggers.any? + %h3 + Use CURL + + %p.light + Copy the token above and set your branch or tag name. This is the reference that will be rebuild. + + + %pre + :plain + curl -X POST \ + -F token=TOKEN \ + #{ci_build_trigger_url(@project.id, 'REF_NAME')} + %h3 + Use .gitlab-ci.yml + + %p.light + Copy the snippet to + %i .gitlab-ci.yml + of dependent project. + At the end of your build it will trigger this project to rebuilt. + + %pre + :plain + trigger: + type: deploy + script: + - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@project.id, 'REF_NAME')}" + %h3 + Pass build variables + + %p.light + Add + %strong variables[VARIABLE]=VALUE + to API request. + The value of variable could then be used to distinguish triggered build from normal one. + + %pre + :plain + curl -X POST \ + -F token=TOKEN \ + -F "variables[RUN_NIGHTLY_BUILD]=true" \ + #{ci_build_trigger_url(@project.id, 'REF_NAME')} diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml new file mode 100644 index 00000000000..308b217ea78 --- /dev/null +++ b/app/views/ci/user_sessions/new.html.haml @@ -0,0 +1,8 @@ +.login-block + %h2 Login using GitLab account + %p.light + Make sure you have account on GitLab server + = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink + %hr + = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' ) + diff --git a/app/views/ci/user_sessions/show.html.haml b/app/views/ci/user_sessions/show.html.haml new file mode 100644 index 00000000000..43f64a429b2 --- /dev/null +++ b/app/views/ci/user_sessions/show.html.haml @@ -0,0 +1,15 @@ += image_tag user_avatar_url(current_user, 90), class: 'avatar avatar-inline avatar-tile s90', alt: '' +%h3 + Hi, #{@user.name} + + - if @user.is_admin + %span.label.label-success Admin + +.profile-block + %p + %span.light Email: + %strong= @user.email + + %p + %span.light GitLab profile: + %strong= link_to @user.username, GitlabCi.config.gitlab_server.url + '/u/' + @user.username, target: "_blank" diff --git a/app/views/ci/variables/show.html.haml b/app/views/ci/variables/show.html.haml new file mode 100644 index 00000000000..5cced18a09f --- /dev/null +++ b/app/views/ci/variables/show.html.haml @@ -0,0 +1,37 @@ +%h3 Secret Variables +%p.light + These variables will be set to environment by the runner and will be hidden in the build log. + %br + So you can use them for passwords, secret keys or whatever you want. + +%hr + + += nested_form_for @project, url: url_for(controller: 'ci/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| + - if @project.errors.any? + #error_explanation + %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" + .alert.alert-error + %ul + - @project.errors.full_messages.each do |msg| + %li= msg + + = f.fields_for :variables do |variable_form| + .form-group + = variable_form.label :key, 'Key', class: 'control-label' + .col-sm-10 + = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE" + + .form-group + = variable_form.label :value, 'Value', class: 'control-label' + .col-sm-10 + = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: "" + + = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10' + %hr + %p + .clearfix + = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' + + .form-actions + = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url diff --git a/app/views/ci/web_hooks/index.html.haml b/app/views/ci/web_hooks/index.html.haml new file mode 100644 index 00000000000..92c43cd1d9d --- /dev/null +++ b/app/views/ci/web_hooks/index.html.haml @@ -0,0 +1,92 @@ +%h3 + Web hooks + +%p.light + Web Hooks can be used for binding events when build completed. + +%hr.clearfix + += form_for [:ci, @project, @web_hook], html: { class: 'form-horizontal' } do |f| + -if @web_hook.errors.any? + .alert.alert-danger + - @web_hook.errors.full_messages.each do |msg| + %p= msg + .form-group + = f.label :url, "URL", class: 'control-label' + .col-sm-10 + = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' + .form-actions + = f.submit "Add Web Hook", class: "btn btn-create" + +-if @web_hooks.any? + %h4 Activated web hooks (#{@web_hooks.count}) + %table.table + - @web_hooks.each do |hook| + %tr + %td + .clearfix + %span.monospace= hook.url + %td + .pull-right + - if @project.commits.any? + = link_to 'Test Hook', test_ci_project_web_hook_path(@project, hook), class: "btn btn-sm btn-grouped" + = link_to 'Remove', ci_project_web_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + +%h4 Web Hook data example + +:erb +
    +    
    +      {
    +        "build_id": 2,
    +        "build_name":"rspec_linux"
    +        "build_status": "failed",
    +        "build_started_at": "2014-05-05T18:01:02.563Z",
    +        "build_finished_at": "2014-05-05T18:01:07.611Z",
    +        "project_id": 1,
    +        "project_name": "Brightbox \/ Brightbox Cli",
    +        "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli",
    +        "ref": "master",
    +        "sha": "a26cf5de9ed9827746d4970872376b10d9325f40",
    +        "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
    +        "push_data": {
    +          "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
    +          "after": "a26cf5de9ed9827746d4970872376b10d9325f40",
    +          "ref": "refs\/heads\/master",
    +          "user_id": 1,
    +          "user_name": "Administrator",
    +          "project_id": 5,
    +          "repository": {
    +            "name": "Brightbox Cli",
    +            "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git",
    +            "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.",
    +            "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli"
    +          },
    +          "commits": [
    +            {
    +              "id": "a26cf5de9ed9827746d4970872376b10d9325f40",
    +              "message": "Release v1.2.2",
    +              "timestamp": "2014-04-22T16:46:42+03:00",
    +              "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40",
    +              "author": {
    +                "name": "Paul Thornthwaite",
    +                "email": "tokengeek@gmail.com"
    +              }
    +            },
    +            {
    +              "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
    +              "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.",
    +              "timestamp": "2014-04-11T18:17:26+03:00",
    +              "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c",
    +              "author": {
    +                "name": "Paul Thornthwaite",
    +                "email": "tokengeek@gmail.com"
    +              }
    +            }
    +          ],
    +          "total_commits_count": 2,
    +          "ci_yaml_file":"rspec_linux:\r\n  script: ls\r\n"
    +        }
    +      }
    +    
    +  
    diff --git a/app/views/layouts/ci/_head.html.haml b/app/views/layouts/ci/_head.html.haml new file mode 100644 index 00000000000..871752c9812 --- /dev/null +++ b/app/views/layouts/ci/_head.html.haml @@ -0,0 +1,11 @@ +%head + %meta{charset: "utf-8"} + %meta{content: "GitLab Continuous Integration", name: "description"} + %title GitLab CI + = stylesheet_link_tag "ci/application", :media => "all" + = javascript_include_tag "ci/application" + = csrf_meta_tags + = favicon_link_tag 'ci/favicon.ico' + :erb + + diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml new file mode 100644 index 00000000000..bce3ce77031 --- /dev/null +++ b/app/views/layouts/ci/_info.html.haml @@ -0,0 +1,9 @@ +.container + - if alert || notice + - if alert + .alert.alert-danger= alert + - if notice + .alert.alert-info= notice + + - if current_user && current_user.is_admin && Ci::Runner.count.zero? + = render 'ci/shared/no_runners' diff --git a/app/views/layouts/ci/_nav.html.haml b/app/views/layouts/ci/_nav.html.haml new file mode 100644 index 00000000000..4e944d4d0d6 --- /dev/null +++ b/app/views/layouts/ci/_nav.html.haml @@ -0,0 +1,32 @@ +.navbar.navbar-static-top.navbar-ci + .container + .navbar-header + %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} + %span.sr-only Toggle navigation + %i.fa-reorder + + = link_to 'GitLab CI', ci_root_path, class: "navbar-brand" + + .collapse.navbar-collapse + %ul.nav.navbar-nav + - if current_user && current_user.is_admin + %li + = link_to ci_admin_projects_path do + Admin + %li + = link_to 'Help', ci_help_path + + %ul.nav.navbar-nav.pull-right + - if current_user + %li + = link_to ci_user_sessions_path do + .profile-holder + = image_tag user_avatar_url(current_user, 64), class: 'avatar s32', alt: '' + %span= current_user.name + %li + = link_to ci_user_sessions_path, class: "logout", method: :delete do + %i.fa-signout + Logout + - else + %li + = link_to "Login with GitLab", auth_ci_user_sessions_path, no_turbolink.merge(class: 'btn btn-success btn-login') diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml new file mode 100644 index 00000000000..792a5f1e4dd --- /dev/null +++ b/app/views/layouts/ci/_nav_admin.html.haml @@ -0,0 +1,28 @@ +%ul.nav.nav-pills.nav-stacked.admin-menu + = nav_link path: 'projects' do + = link_to ci_admin_projects_path do + %i.fa-list-alt + Projects + = nav_link path: 'events' do + = link_to ci_admin_events_path do + %i.fa-book + Events + = nav_link path: 'runners#index' do + = link_to ci_admin_runners_path do + %i.fa-cog + Runners + %small.pull-right + = Ci::Runner.count(:all) + = nav_link path: 'builds' do + = link_to ci_admin_builds_path do + %i.fa-link + Builds + %small.pull-right + = Ci::Build.count(:all) + %li + %hr + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = link_to ci_admin_application_settings_path do + %i.fa-cogs + %span + Settings diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml new file mode 100644 index 00000000000..24ee1609d25 --- /dev/null +++ b/app/views/layouts/ci/_nav_project.html.haml @@ -0,0 +1,40 @@ +%ul.nav.nav-pills.nav-stacked.project-menu + = nav_link path: 'projects#show' do + = link_to ci_project_path(@project) do + %i.fa-list-alt + Commits + %small.pull-right= @project.commits.count + = nav_link path: 'charts#show' do + = link_to ci_project_charts_path(@project) do + %i.fa-bar-chart + Charts + = nav_link path: ['runners#index', 'runners#show'] do + = link_to ci_project_runners_path(@project) do + %i.fa-cog + Runners + = nav_link path: 'variables#index' do + = link_to ci_project_variables_path(@project) do + %i.fa-code + Variables + = nav_link path: 'web_hooks#index' do + = link_to ci_project_web_hooks_path(@project) do + %i.fa-link + Web Hooks + = nav_link path: 'triggers#index' do + = link_to ci_project_triggers_path(@project) do + %i.fa-retweet + Triggers + = nav_link path: 'services#index' do + = link_to ci_project_services_path(@project) do + %i.fa-share + Services + = nav_link path: 'events#index' do + = link_to ci_project_events_path(@project) do + %i.fa-book + Events + %li + %hr + = nav_link path: 'projects#edit' do + = link_to edit_ci_project_path(@project) do + %i.fa-cogs + Settings diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml new file mode 100644 index 00000000000..71b767cc4f1 --- /dev/null +++ b/app/views/layouts/ci/admin.html.haml @@ -0,0 +1,17 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/ci/head' + %body{ :'data-page' => body_data_page } + = render 'layouts/ci/nav' + = render 'layouts/ci/info' + - if content_for?(:title) + .container.container-title + = yield(:title) + %hr + + .container + .row + .col-md-2.append-bottom-20 + = render 'layouts/ci/nav_admin' + .col-md-10 + = yield diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml new file mode 100644 index 00000000000..7306d378e44 --- /dev/null +++ b/app/views/layouts/ci/application.html.haml @@ -0,0 +1,13 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/ci/head' + %body{ :'data-page' => body_data_page } + = render 'layouts/ci/nav' + = render 'layouts/ci/info' + - if content_for?(:title) + .container.container-title + = yield(:title) + %hr + + .container.container-body + = yield diff --git a/app/views/layouts/ci/empty.html.haml b/app/views/layouts/ci/empty.html.haml new file mode 100644 index 00000000000..a36ebee7ef3 --- /dev/null +++ b/app/views/layouts/ci/empty.html.haml @@ -0,0 +1,13 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/ci/head' + %body{ :'data-page' => body_data_page } + = render 'layouts/ci/info' + - if content_for?(:title) + .container.container-title + = yield(:title) + %hr + + .container.container-body + = yield + diff --git a/app/views/layouts/ci/notify.html.haml b/app/views/layouts/ci/notify.html.haml new file mode 100644 index 00000000000..270b206df5e --- /dev/null +++ b/app/views/layouts/ci/notify.html.haml @@ -0,0 +1,19 @@ +%html{lang: "en"} + %head + %meta{content: "text/html; charset=utf-8", "http-equiv" => "Content-Type"} + %title + GitLab CI + + %body + = yield :header + + %table{align: "left", border: "0", cellpadding: "0", cellspacing: "0", style: "padding: 10px 0;", width: "100%"} + %tr + %td{align: "left", style: "margin: 0; padding: 10px;"} + = yield + %br + %tr + %td{align: "left", style: "margin: 0; padding: 10px;"} + %p{style: "font-size:small;color:#777"} + - if @project + You're receiving this notification because you are the one who triggered a build on the #{@project.name} project. diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml new file mode 100644 index 00000000000..d0c0861669d --- /dev/null +++ b/app/views/layouts/ci/project.html.haml @@ -0,0 +1,26 @@ +!!! 5 +%html{ lang: "en"} + = render 'layouts/ci/head' + %body{ :'data-page' => body_data_page } + = render 'layouts/ci/nav' + = render 'layouts/ci/info' + .container + %h3.project-title + = @project.name + - if @project.public + %small + %i.fa-globe + Public + + .pull-right + = link_to 'View on GitLab', @project.gitlab_url, no_turbolink.merge( class: 'btn btn-sm' ) + %hr + .container + - if current_user && current_user.can_manage_project?(@project.gitlab_id) + .row + .col-md-2.append-bottom-20 + = render 'layouts/ci/nav_project' + .col-md-10 + = yield + - else + = yield diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb new file mode 100644 index 00000000000..ebb43570e2a --- /dev/null +++ b/app/workers/ci/hip_chat_notifier_worker.rb @@ -0,0 +1,19 @@ +module Ci + class HipChatNotifierWorker + include Sidekiq::Worker + + def perform(message, options={}) + room = options.delete('room') + token = options.delete('token') + server = options.delete('server') + name = options.delete('service_name') + client_opts = { + api_version: 'v2', + server_url: server + } + + client = HipChat::Client.new(token, client_opts) + client[room].send(name, message, options.symbolize_keys) + end + end +end diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb new file mode 100644 index 00000000000..3bbb9b4bec7 --- /dev/null +++ b/app/workers/ci/slack_notifier_worker.rb @@ -0,0 +1,10 @@ +module Ci + class SlackNotifierWorker + include Sidekiq::Worker + + def perform(webhook_url, message, options={}) + notifier = Slack::Notifier.new(webhook_url) + notifier.ping(message, options) + end + end +end diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb new file mode 100644 index 00000000000..0bb83845572 --- /dev/null +++ b/app/workers/ci/web_hook_worker.rb @@ -0,0 +1,9 @@ +module Ci + class WebHookWorker + include Sidekiq::Worker + + def perform(hook_id, data) + Ci::WebHook.find(hook_id).execute data + end + end +end diff --git a/bin/background_jobs b/bin/background_jobs index a4895cf6586..d4578f6a222 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 } load_ok() diff --git a/bin/ci/upgrade.rb b/bin/ci/upgrade.rb new file mode 100644 index 00000000000..aab4f60ec60 --- /dev/null +++ b/bin/ci/upgrade.rb @@ -0,0 +1,3 @@ +require_relative "../lib/ci/upgrader" + +Ci::Upgrader.new.execute diff --git a/config/environments/development.rb b/config/environments/development.rb index 03af7f07864..d7d6aed1602 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -24,6 +24,11 @@ # Expands the lines which load the assets # config.assets.debug = true + + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true # For having correct urls in mails config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } diff --git a/config/gitlab_ci.yml b/config/gitlab_ci.yml new file mode 100644 index 00000000000..03a86307f40 --- /dev/null +++ b/config/gitlab_ci.yml @@ -0,0 +1,19 @@ +development: + gitlab_server: + url: 'http://gitlab.dev' + app_id: 'cfda7ec2551af42d06acc6dbda9087dbdc8d45b7e2bc240f498fad3f84ff4044' + app_secret: 'd1802d55db9c1aedc950812a9489e2659fa1430dc488babde949bc9c409cc01b' + + gitlab_ci: + host: 'http://ci.gitlab.dev' + port: 80 + https: false +test: + gitlab_server: + url: 'http://demo.gitlab.com/' + app_id: '' + app_secret: '' + gitlab_ci: + host: localhost + port: 80 + https: false diff --git a/config/gitlab_ci.yml.example b/config/gitlab_ci.yml.example new file mode 100644 index 00000000000..dd33daa5578 --- /dev/null +++ b/config/gitlab_ci.yml.example @@ -0,0 +1,68 @@ +# If you change this file in a Merge Request, please also create +# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests + +defaults: &defaults + gitlab_server: + url: 'https://gitlab.example.com/' # Replace with your gitlab server url + app_id: '' + app_secret: '' + + ## Gitlab CI settings + gitlab_ci: + ## Web server settings + host: localhost + port: 80 + https: false + + ## Email settings + # Email address used in the "From" field in mails sent by GitLab-CI + email_from: gitlab-ci@localhost + + # Email address of your support contact (default: same as email_from) + support_email: support@localhost + + # Default project notifications settings: + # + # Send emails only on broken builds (default: true) + # all_broken_builds: true + # + # Add pusher to recipients list (default: false) + # add_pusher: true + + # The location where build traces are stored (default: builds/). Relative paths are relative to Rails.root + # builds_path: builds/ + + ## Backup settings + backup: + path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/) + # keep_time: 604800 # default: 0 (forever) (in seconds) + # upload: + # # Fog storage connection settings, see http://fog.io/storage/ . + # connection: + # provider: AWS + # region: eu-west-1 + # aws_access_key_id: AKIAKIAKI + # aws_secret_access_key: 'secret123' + # # The remote 'directory' to store your backups. For S3, this would be the bucket name. + # remote_directory: 'my.s3.bucket' + # # Use multipart uploads when file size reaches 100MB, see + # # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html + # multipart_chunk_size: 104857600 + + +development: + <<: *defaults + +test: + <<: *defaults + gitlab_server: + url: 'http://demo.gitlab.com/' + app_id: 'id' + app_secret: 'secret' + gitlab_ci: + host: localhost + port: 80 + https: false + +production: + <<: *defaults diff --git a/config/gitlab_ci.yml.example.development b/config/gitlab_ci.yml.example.development new file mode 100644 index 00000000000..d23c4daf464 --- /dev/null +++ b/config/gitlab_ci.yml.example.development @@ -0,0 +1,19 @@ +development: + gitlab_server: + url: 'http://localhost:3000' + app_id: '' + app_secret: '' + + gitlab_ci: + host: localhost + port: 9000 + https: false +test: + gitlab_server: + url: 'http://demo.gitlab.com/' + app_id: '' + app_secret: '' + gitlab_ci: + host: localhost + port: 80 + https: false diff --git a/config/initializers/3_ci_settings.rb b/config/initializers/3_ci_settings.rb new file mode 100644 index 00000000000..5cdff48d316 --- /dev/null +++ b/config/initializers/3_ci_settings.rb @@ -0,0 +1,61 @@ +module Ci + class Settings < Settingslogic + source "#{Rails.root}/config/gitlab_ci.yml" + namespace Rails.env + + class << self + def gitlab_ci_on_non_standard_port? + ![443, 80].include?(gitlab_ci.port.to_i) + end + + private + + def build_gitlab_ci_url + if gitlab_ci_on_non_standard_port? + custom_port = ":#{gitlab_ci.port}" + else + custom_port = nil + end + [ gitlab_ci.protocol, + "://", + gitlab_ci.host, + custom_port, + gitlab_ci.relative_url_root + ].join('') + end + end + end +end + + +# +# GitlabCi +# +Ci::Settings['gitlab_ci'] ||= Settingslogic.new({}) +Ci::Settings.gitlab_ci['https'] = false if Ci::Settings.gitlab_ci['https'].nil? +Ci::Settings.gitlab_ci['host'] ||= 'localhost' +Ci::Settings.gitlab_ci['port'] ||= Ci::Settings.gitlab_ci.https ? 443 : 80 +Ci::Settings.gitlab_ci['relative_url_root'] ||= (ENV['RAILS_RELATIVE_URL_ROOT'] || '') + '/ci' +Ci::Settings.gitlab_ci['protocol'] ||= Ci::Settings.gitlab_ci.https ? "https" : "http" +Ci::Settings.gitlab_ci['email_from'] ||= "gitlab-ci@#{Ci::Settings.gitlab_ci.host}" +Ci::Settings.gitlab_ci['support_email'] ||= Ci::Settings.gitlab_ci.email_from +Ci::Settings.gitlab_ci['all_broken_builds'] = true if Ci::Settings.gitlab_ci['all_broken_builds'].nil? +Ci::Settings.gitlab_ci['add_pusher'] = false if Ci::Settings.gitlab_ci['add_pusher'].nil? +Ci::Settings.gitlab_ci['url'] ||= Ci::Settings.send(:build_gitlab_ci_url) +Ci::Settings.gitlab_ci['builds_path'] = File.expand_path(Ci::Settings.gitlab_ci['builds_path'] || "builds/", Rails.root + '/ci') + +# Compatibility with old config +Ci::Settings['gitlab_server_urls'] ||= Ci::Settings['allowed_gitlab_urls'] + +# +# Backup +# +Ci::Settings['backup'] ||= Settingslogic.new({}) +Ci::Settings.backup['keep_time'] ||= 0 +Ci::Settings.backup['path'] = File.expand_path(Ci::Settings.backup['path'] || "tmp/backups/", Rails.root) +Ci::Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) +# Convert upload connection settings to use symbol keys, to make Fog happy +if Ci::Settings.backup['upload']['connection'] + Ci::Settings.backup['upload']['connection'] = Hash[Ci::Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }] +end +Ci::Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 diff --git a/config/initializers/3_grit_ext.rb b/config/initializers/3_grit_ext.rb deleted file mode 100644 index 6540ac839cb..00000000000 --- a/config/initializers/3_grit_ext.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'grit' - -Grit::Git.git_binary = Gitlab.config.git.bin_path -Grit::Git.git_timeout = Gitlab.config.git.timeout -Grit::Git.git_max_size = Gitlab.config.git.max_size diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb new file mode 100644 index 00000000000..60a30bf3bb7 --- /dev/null +++ b/config/initializers/4_ci_app.rb @@ -0,0 +1,10 @@ +module GitlabCi + VERSION = Gitlab::VERSION + REVISION = Gitlab::REVISION + + REGISTRATION_TOKEN = SecureRandom.hex(10) + + def self.config + Ci::Settings + end +end diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb deleted file mode 100644 index 7f73546ac89..00000000000 --- a/config/initializers/7_omniauth.rb +++ /dev/null @@ -1,28 +0,0 @@ -if Gitlab::LDAP::Config.enabled? - module OmniAuth::Strategies - server = Gitlab.config.ldap.servers.values.first - klass = server['provider_class'] - const_set(klass, Class.new(LDAP)) unless klass == 'LDAP' - end - - OmniauthCallbacksController.class_eval do - server = Gitlab.config.ldap.servers.values.first - alias_method server['provider_name'], :ldap - end -end - -OmniAuth.config.full_host = Settings.gitlab['url'] -OmniAuth.config.allowed_request_methods = [:post] -#In case of auto sign-in, the GET method is used (users don't get to click on a button) -OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? -OmniAuth.config.before_request_phase do |env| - OmniAuth::RequestForgeryProtection.new(env).call -end - -if Gitlab.config.omniauth.enabled - Gitlab.config.omniauth.providers.each do |provider| - if provider['name'] == 'kerberos' - require 'omniauth-kerberos' - end - end -end diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb new file mode 100644 index 00000000000..d831a1838ed --- /dev/null +++ b/config/initializers/connection_fix.rb @@ -0,0 +1,32 @@ +# from http://gist.github.com/238999 +# +# If your workers are inactive for a long period of time, they'll lose +# their MySQL connection. +# +# This hack ensures we re-connect whenever a connection is +# lost. Because, really. why not? +# +# Stick this in RAILS_ROOT/config/initializers/connection_fix.rb (or somewhere similar) +# +# From: +# http://coderrr.wordpress.com/2009/01/08/activerecord-threading-issues-and-resolutions/ + +if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) + module ActiveRecord::ConnectionAdapters + class Mysql2Adapter + alias_method :execute_without_retry, :execute + + def execute(*args) + execute_without_retry(*args) + rescue ActiveRecord::StatementInvalid => e + if e.message =~ /server has gone away/i + warn "Server timed out, retrying" + reconnect! + retry + else + raise e + end + end + end + end +end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000000..43adac8b2c6 --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Gitlab::Application.config.action_dispatch.cookies_serializer = :hybrid diff --git a/config/initializers/8_default_url_options.rb b/config/initializers/default_url_options.rb similarity index 80% rename from config/initializers/8_default_url_options.rb rename to config/initializers/default_url_options.rb index 8fd27b1d88e..f9f88f95db9 100644 --- a/config/initializers/8_default_url_options.rb +++ b/config/initializers/default_url_options.rb @@ -8,4 +8,4 @@ default_url_options[:port] = Gitlab.config.gitlab.port end -Rails.application.routes.default_url_options = default_url_options +Gitlab::Application.routes.default_url_options = default_url_options diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example index b1bbcca1d61..2155ea14562 100644 --- a/config/initializers/rack_attack.rb.example +++ b/config/initializers/rack_attack.rb.example @@ -4,13 +4,13 @@ # If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests paths_to_be_protected = [ - "#{Rails.application.config.relative_url_root}/users/password", - "#{Rails.application.config.relative_url_root}/users/sign_in", - "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session.json", - "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session", - "#{Rails.application.config.relative_url_root}/users", - "#{Rails.application.config.relative_url_root}/users/confirmation", - "#{Rails.application.config.relative_url_root}/unsubscribes/" + "#{Gitlab::Application.config.relative_url_root}/users/password", + "#{Gitlab::Application.config.relative_url_root}/users/sign_in", + "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session.json", + "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session", + "#{Gitlab::Application.config.relative_url_root}/users", + "#{Gitlab::Application.config.relative_url_root}/users/confirmation", + "#{Gitlab::Application.config.relative_url_root}/unsubscribes/" ] diff --git a/config/initializers/6_rack_profiler.rb b/config/initializers/rack_profiler.rb similarity index 81% rename from config/initializers/6_rack_profiler.rb rename to config/initializers/rack_profiler.rb index 1d958904e8f..7710eeac453 100644 --- a/config/initializers/6_rack_profiler.rb +++ b/config/initializers/rack_profiler.rb @@ -2,7 +2,7 @@ require 'rack-mini-profiler' # initialization is skipped so trigger it - Rack::MiniProfilerRails.initialize!(Rails.application) + Rack::MiniProfilerRails.initialize!(Gitlab::Application) Rack::MiniProfiler.config.position = 'right' Rack::MiniProfiler.config.start_hidden = false diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 62a54bc8c63..1b518c3becf 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -24,3 +24,27 @@ def find_secure_token Gitlab::Application.config.secret_token = find_secure_token Gitlab::Application.config.secret_key_base = find_secure_token + +# CI +def generate_new_secure_token + SecureRandom.hex(64) +end + +if Gitlab::Application.secrets.db_key_base.blank? + warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`" + + all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml') + all_secrets ||= {} + + # generate secrets + env_secrets = all_secrets[Rails.env.to_s] || {} + env_secrets['db_key_base'] ||= generate_new_secure_token + all_secrets[Rails.env.to_s] = env_secrets + + # save secrets + File.open('config/secrets.yml', 'w', 0600) do |file| + file.write(YAML.dump(all_secrets)) + end + + Gitlab::Application.secrets.db_key_base = env_secrets['db_key_base'] +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 6d274cd95a1..c3bee1c7cec 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -11,5 +11,5 @@ secure: Gitlab.config.gitlab.https, httponly: true, expire_after: Settings.gitlab['session_expire_delay'] * 60, - path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root + path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root ) diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/sidekiq.rb similarity index 100% rename from config/initializers/4_sidekiq.rb rename to config/initializers/sidekiq.rb diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index d9042c652bb..e6d5600edb7 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,4 +1,4 @@ -app = Rails.application +app = Gitlab::Application if app.config.serve_static_assets # The `ActionDispatch::Static` middleware intercepts requests for static files diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index f3db5b7476e..d8bf0878a3d 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -32,10 +32,11 @@ en: send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' updated: 'Your password was changed successfully. You are now signed in.' updated_not_active: 'Your password was changed successfully.' - send_paranoid_instructions: "If your e-mail exists on our database, you will receive a password recovery link on your e-mail" + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." confirmations: send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' - send_paranoid_instructions: 'If your e-mail exists on our database, you will receive an email with instructions about how to confirm your account in a few minutes.' + send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.' confirmed: 'Your account was successfully confirmed. You are now signed in.' registrations: signed_up: 'Welcome! You have signed up successfully.' @@ -57,4 +58,4 @@ en: reset_password_instructions: subject: 'Reset password instructions' unlock_instructions: - subject: 'Unlock Instructions' \ No newline at end of file + subject: 'Unlock Instructions' diff --git a/config/routes.rb b/config/routes.rb index d7307a61ede..74544d63d86 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,105 @@ require 'api/api' Gitlab::Application.routes.draw do + namespace :ci do + # CI API + Ci::API::API.logger Rails.logger + mount Ci::API::API => '/api' + + resource :lint, only: [:show, :create] + + resource :help do + get :oauth2 + end + + resources :projects do + collection do + post :add + get :gitlab + end + + member do + get :status, to: 'projects#badge' + get :integration + post :build + post :toggle_shared_runners + get :dumped_yaml + end + + resources :services, only: [:index, :edit, :update] do + member do + get :test + end + end + + resource :charts, only: [:show] + + resources :refs, constraints: { ref_id: /.*/ }, only: [] do + resources :commits, only: [:show] do + member do + get :status + get :cancel + end + end + end + + resources :builds, only: [:show] do + member do + get :cancel + get :status + post :retry + end + end + + resources :web_hooks, only: [:index, :create, :destroy] do + member do + get :test + end + end + + resources :triggers, only: [:index, :create, :destroy] + + resources :runners, only: [:index, :edit, :update, :destroy, :show] do + member do + get :resume + get :pause + end + end + + resources :runner_projects, only: [:create, :destroy] + + resources :events, only: [:index] + resource :variables, only: [:show, :update] + end + + resource :user_sessions do + get :auth + get :callback + end + + namespace :admin do + resources :runners, only: [:index, :show, :update, :destroy] do + member do + put :assign_all + get :resume + get :pause + end + end + + resources :events, only: [:index] + + resources :projects do + resources :runner_projects + end + + resources :builds, only: :index + + resource :application_settings, only: [:show, :update] + end + + root to: 'projects#index' + end + use_doorkeeper do controllers applications: 'oauth/applications', authorized_applications: 'oauth/authorized_applications', diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 00000000000..8122f7cc69c --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,8 @@ +# Use this file to easily define all of your cron jobs. +# +# If you make changes to this file, please create also an issue on +# https://gitlab.com/gitlab-org/omnibus-gitlab/issues . This is necessary +# because the omnibus packages manage cron jobs using Chef instead of Whenever. +every 1.hour do + rake "ci:schedule_builds" +end diff --git a/config/secrets.yml b/config/secrets.yml new file mode 100644 index 00000000000..f63c74d0688 --- /dev/null +++ b/config/secrets.yml @@ -0,0 +1,3 @@ +--- +development: + db_key_base: 53ab5c413f37a5a87df3c7e55dc49924793c44b9a40834af258f75ce3cc71067478b7c1f999bf22d9cfb9e6dedffda989dc462684f8c869705f735a92b7230ed diff --git a/config/secrets.yml.example b/config/secrets.yml.example new file mode 100644 index 00000000000..6b408ac6031 --- /dev/null +++ b/config/secrets.yml.example @@ -0,0 +1,12 @@ +production: + # db_key_base is used to encrypt for Variables. Ensure that you don't lose it. + # If you change or lose this key you will be unable to access variables stored in database. + # Make sure the secret is at least 30 characters and all random, + # no regular words or you'll be exposed to dictionary attacks. + # db_key_base: + +development: + db_key_base: development + +test: + db_key_base: test diff --git a/config/sidekiq.yml.example b/config/sidekiq.yml.example new file mode 100644 index 00000000000..c691db67c6c --- /dev/null +++ b/config/sidekiq.yml.example @@ -0,0 +1,2 @@ +-- +:concurrency: 5 \ No newline at end of file diff --git a/db/ci/migrate/20121004140911_create_projects.rb b/db/ci/migrate/20121004140911_create_projects.rb new file mode 100644 index 00000000000..a9fee3aa6c8 --- /dev/null +++ b/db/ci/migrate/20121004140911_create_projects.rb @@ -0,0 +1,14 @@ +class CreateProjects < ActiveRecord::Migration + def up + create_table :projects do |t| + t.string :name, null: false + t.string :path, null: false + t.integer :timeout, null: false, default: 1800 + t.text :scripts, null: false + t.timestamps + end + end + + def down + end +end diff --git a/db/ci/migrate/20121004165038_create_builds.rb b/db/ci/migrate/20121004165038_create_builds.rb new file mode 100644 index 00000000000..547803489fb --- /dev/null +++ b/db/ci/migrate/20121004165038_create_builds.rb @@ -0,0 +1,15 @@ +class CreateBuilds < ActiveRecord::Migration + def up + create_table :builds do |t| + t.integer :project_id + t.string :commit_ref + t.string :status + t.datetime :finished_at + t.text :trace + t.timestamps + end + end + + def down + end +end diff --git a/db/ci/migrate/20121101091638_devise_create_users.rb b/db/ci/migrate/20121101091638_devise_create_users.rb new file mode 100644 index 00000000000..2099d998fa4 --- /dev/null +++ b/db/ci/migrate/20121101091638_devise_create_users.rb @@ -0,0 +1,46 @@ +class DeviseCreateUsers < ActiveRecord::Migration + def change + create_table(:users) do |t| + ## Database authenticatable + t.string :email, :null => false, :default => "" + t.string :encrypted_password, :null => false, :default => "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, :default => 0 + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, :default => 0 # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + ## Token authenticatable + # t.string :authentication_token + + + t.timestamps + end + + add_index :users, :email, :unique => true + add_index :users, :reset_password_token, :unique => true + # add_index :users, :confirmation_token, :unique => true + # add_index :users, :unlock_token, :unique => true + # add_index :users, :authentication_token, :unique => true + end +end diff --git a/db/ci/migrate/20121101121639_add_token_to_project.rb b/db/ci/migrate/20121101121639_add_token_to_project.rb new file mode 100644 index 00000000000..bb66677b6b1 --- /dev/null +++ b/db/ci/migrate/20121101121639_add_token_to_project.rb @@ -0,0 +1,5 @@ +class AddTokenToProject < ActiveRecord::Migration + def change + add_column :projects, :token, :string, null: true + end +end diff --git a/db/ci/migrate/20121106143042_add_ref_functionality.rb b/db/ci/migrate/20121106143042_add_ref_functionality.rb new file mode 100644 index 00000000000..0c26571e305 --- /dev/null +++ b/db/ci/migrate/20121106143042_add_ref_functionality.rb @@ -0,0 +1,10 @@ +class AddRefFunctionality < ActiveRecord::Migration + def change + rename_column :builds, :commit_ref, :ref + add_column :builds, :sha, :string + add_column :projects, :default_ref, :string + end + + def down + end +end diff --git a/db/ci/migrate/20121108160657_add_gitlab_url_to_project.rb b/db/ci/migrate/20121108160657_add_gitlab_url_to_project.rb new file mode 100644 index 00000000000..8a4e8fd666f --- /dev/null +++ b/db/ci/migrate/20121108160657_add_gitlab_url_to_project.rb @@ -0,0 +1,5 @@ +class AddGitlabUrlToProject < ActiveRecord::Migration + def change + add_column :projects, :gitlab_url, :string, null: true + end +end diff --git a/db/ci/migrate/20121108174237_add_started_at_to_build.rb b/db/ci/migrate/20121108174237_add_started_at_to_build.rb new file mode 100644 index 00000000000..b4d65c75004 --- /dev/null +++ b/db/ci/migrate/20121108174237_add_started_at_to_build.rb @@ -0,0 +1,5 @@ +class AddStartedAtToBuild < ActiveRecord::Migration + def change + add_column :builds, :started_at, :datetime, null: true + end +end diff --git a/db/ci/migrate/20121115094430_increate_trace_colunm_limit.rb b/db/ci/migrate/20121115094430_increate_trace_colunm_limit.rb new file mode 100644 index 00000000000..5853f440f59 --- /dev/null +++ b/db/ci/migrate/20121115094430_increate_trace_colunm_limit.rb @@ -0,0 +1,8 @@ +class IncreateTraceColunmLimit < ActiveRecord::Migration + def up + change_column :builds, :trace, :text, :limit => 1073741823 + end + + def down + end +end diff --git a/db/ci/migrate/20121115132252_add_tmp_file_to_build.rb b/db/ci/migrate/20121115132252_add_tmp_file_to_build.rb new file mode 100644 index 00000000000..a9a4e36b5ba --- /dev/null +++ b/db/ci/migrate/20121115132252_add_tmp_file_to_build.rb @@ -0,0 +1,5 @@ +class AddTmpFileToBuild < ActiveRecord::Migration + def change + add_column :builds, :tmp_file, :string + end +end diff --git a/db/ci/migrate/20121116144312_add_before_sha_to_build.rb b/db/ci/migrate/20121116144312_add_before_sha_to_build.rb new file mode 100644 index 00000000000..7b8cfd93caa --- /dev/null +++ b/db/ci/migrate/20121116144312_add_before_sha_to_build.rb @@ -0,0 +1,5 @@ +class AddBeforeShaToBuild < ActiveRecord::Migration + def change + add_column :builds, :before_sha, :string, null: true + end +end diff --git a/db/ci/migrate/20121224092350_add_schedule_to_projects.rb b/db/ci/migrate/20121224092350_add_schedule_to_projects.rb new file mode 100644 index 00000000000..fb3155f1159 --- /dev/null +++ b/db/ci/migrate/20121224092350_add_schedule_to_projects.rb @@ -0,0 +1,6 @@ +class AddScheduleToProjects < ActiveRecord::Migration + def change + add_column :projects, :always_build, :boolean, default: false, null: false + add_column :projects, :polling_interval, :string, null: true + end +end diff --git a/db/ci/migrate/20130114153451_change_schedule_invertal.rb b/db/ci/migrate/20130114153451_change_schedule_invertal.rb new file mode 100644 index 00000000000..accf3eef473 --- /dev/null +++ b/db/ci/migrate/20130114153451_change_schedule_invertal.rb @@ -0,0 +1,25 @@ +class ChangeScheduleInvertal < ActiveRecord::Migration + def up + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + connection.execute(%q{ + ALTER TABLE projects + ALTER COLUMN polling_interval + TYPE integer USING CAST(polling_interval AS integer) + }) + else + change_column :projects, :polling_interval, :integer, null: true + end + end + + def down + if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' + connection.execute(%q{ + ALTER TABLE projects + ALTER COLUMN polling_interval + TYPE integer USING CAST(polling_interval AS varchar) + }) + else + change_column :projects, :polling_interval, :string, null: true + end + end +end diff --git a/db/ci/migrate/20130129121754_add_public_flag_to_project.rb b/db/ci/migrate/20130129121754_add_public_flag_to_project.rb new file mode 100644 index 00000000000..2bfe52f0df4 --- /dev/null +++ b/db/ci/migrate/20130129121754_add_public_flag_to_project.rb @@ -0,0 +1,5 @@ +class AddPublicFlagToProject < ActiveRecord::Migration + def change + add_column :projects, :public, :boolean, null: false, default: false + end +end diff --git a/db/ci/migrate/20130531112551_add_data_field_to_build.rb b/db/ci/migrate/20130531112551_add_data_field_to_build.rb new file mode 100644 index 00000000000..ff897bce448 --- /dev/null +++ b/db/ci/migrate/20130531112551_add_data_field_to_build.rb @@ -0,0 +1,5 @@ +class AddDataFieldToBuild < ActiveRecord::Migration + def change + add_column :builds, :push_data, :text + end +end diff --git a/db/ci/migrate/20130531122131_remove_path_field_from_project.rb b/db/ci/migrate/20130531122131_remove_path_field_from_project.rb new file mode 100644 index 00000000000..684c16470a4 --- /dev/null +++ b/db/ci/migrate/20130531122131_remove_path_field_from_project.rb @@ -0,0 +1,8 @@ +class RemovePathFieldFromProject < ActiveRecord::Migration + def up + remove_column :projects, :path + end + + def down + end +end diff --git a/db/ci/migrate/20130531125905_create_runners.rb b/db/ci/migrate/20130531125905_create_runners.rb new file mode 100644 index 00000000000..2619394f51b --- /dev/null +++ b/db/ci/migrate/20130531125905_create_runners.rb @@ -0,0 +1,10 @@ +class CreateRunners < ActiveRecord::Migration + def change + create_table :runners do |t| + t.string :token + t.text :public_key + + t.timestamps + end + end +end diff --git a/db/ci/migrate/20130531133603_add_runner_id_to_build.rb b/db/ci/migrate/20130531133603_add_runner_id_to_build.rb new file mode 100644 index 00000000000..bccc0970835 --- /dev/null +++ b/db/ci/migrate/20130531133603_add_runner_id_to_build.rb @@ -0,0 +1,5 @@ +class AddRunnerIdToBuild < ActiveRecord::Migration + def change + add_column :builds, :runner_id, :integer + end +end diff --git a/db/ci/migrate/20130603130920_remove_users_table.rb b/db/ci/migrate/20130603130920_remove_users_table.rb new file mode 100644 index 00000000000..6948ef265ef --- /dev/null +++ b/db/ci/migrate/20130603130920_remove_users_table.rb @@ -0,0 +1,5 @@ +class RemoveUsersTable < ActiveRecord::Migration + def up + drop_table :users + end +end diff --git a/db/ci/migrate/20130603144030_add_more_fields_to_project.rb b/db/ci/migrate/20130603144030_add_more_fields_to_project.rb new file mode 100644 index 00000000000..0897682285a --- /dev/null +++ b/db/ci/migrate/20130603144030_add_more_fields_to_project.rb @@ -0,0 +1,5 @@ +class AddMoreFieldsToProject < ActiveRecord::Migration + def change + add_column :projects, :ssh_url_to_repo, :string + end +end diff --git a/db/ci/migrate/20130603144959_create_runner_projects.rb b/db/ci/migrate/20130603144959_create_runner_projects.rb new file mode 100644 index 00000000000..c65c8a51bcf --- /dev/null +++ b/db/ci/migrate/20130603144959_create_runner_projects.rb @@ -0,0 +1,10 @@ +class CreateRunnerProjects < ActiveRecord::Migration + def change + create_table :runner_projects do |t| + t.integer :runner_id, null: false + t.integer :project_id, null: false + + t.timestamps + end + end +end diff --git a/db/ci/migrate/20130603161449_add_project_gitlab_id_to_project.rb b/db/ci/migrate/20130603161449_add_project_gitlab_id_to_project.rb new file mode 100644 index 00000000000..3efdbb7af1c --- /dev/null +++ b/db/ci/migrate/20130603161449_add_project_gitlab_id_to_project.rb @@ -0,0 +1,5 @@ +class AddProjectGitlabIdToProject < ActiveRecord::Migration + def change + add_column :projects, :gitlab_id, :integer + end +end diff --git a/db/ci/migrate/20130628142321_add_index_project_id_to_builds.rb b/db/ci/migrate/20130628142321_add_index_project_id_to_builds.rb new file mode 100644 index 00000000000..5f968b06b5d --- /dev/null +++ b/db/ci/migrate/20130628142321_add_index_project_id_to_builds.rb @@ -0,0 +1,5 @@ +class AddIndexProjectIdToBuilds < ActiveRecord::Migration + def change + add_index :builds, :project_id + end +end diff --git a/db/ci/migrate/20130705171042_add_description_to_runner.rb b/db/ci/migrate/20130705171042_add_description_to_runner.rb new file mode 100644 index 00000000000..1e04e98d109 --- /dev/null +++ b/db/ci/migrate/20130705171042_add_description_to_runner.rb @@ -0,0 +1,5 @@ +class AddDescriptionToRunner < ActiveRecord::Migration + def change + add_column :runners, :description, :string + end +end diff --git a/db/ci/migrate/20130710164015_add_db_index.rb b/db/ci/migrate/20130710164015_add_db_index.rb new file mode 100644 index 00000000000..4907fae888b --- /dev/null +++ b/db/ci/migrate/20130710164015_add_db_index.rb @@ -0,0 +1,7 @@ +class AddDbIndex < ActiveRecord::Migration + def change + add_index :builds, :runner_id + add_index :runner_projects, :runner_id + add_index :runner_projects, :project_id + end +end diff --git a/db/ci/migrate/20130816201200_change_push_data_limit.rb b/db/ci/migrate/20130816201200_change_push_data_limit.rb new file mode 100644 index 00000000000..29bd45c2cf9 --- /dev/null +++ b/db/ci/migrate/20130816201200_change_push_data_limit.rb @@ -0,0 +1,5 @@ +class ChangePushDataLimit < ActiveRecord::Migration + def change + change_column :builds, :push_data, :text, :limit => 16777215 + end +end diff --git a/db/ci/migrate/20130906175737_add_sessions_table.rb b/db/ci/migrate/20130906175737_add_sessions_table.rb new file mode 100644 index 00000000000..4c879564a58 --- /dev/null +++ b/db/ci/migrate/20130906175737_add_sessions_table.rb @@ -0,0 +1,12 @@ +class AddSessionsTable < ActiveRecord::Migration + def change + create_table :sessions do |t| + t.string :session_id, :null => false + t.text :data + t.timestamps + end + + add_index :sessions, :session_id + add_index :sessions, :updated_at + end +end diff --git a/db/ci/migrate/20131023103430_add_allow_git_fetch_to_project.rb b/db/ci/migrate/20131023103430_add_allow_git_fetch_to_project.rb new file mode 100644 index 00000000000..900ea913728 --- /dev/null +++ b/db/ci/migrate/20131023103430_add_allow_git_fetch_to_project.rb @@ -0,0 +1,5 @@ +class AddAllowGitFetchToProject < ActiveRecord::Migration + def change + add_column :projects, :allow_git_fetch, :boolean, default: true, null: false + end +end diff --git a/db/ci/migrate/20131120155545_add_email_notification_fields_to_project.rb b/db/ci/migrate/20131120155545_add_email_notification_fields_to_project.rb new file mode 100644 index 00000000000..e0f4943d40f --- /dev/null +++ b/db/ci/migrate/20131120155545_add_email_notification_fields_to_project.rb @@ -0,0 +1,7 @@ +class AddEmailNotificationFieldsToProject < ActiveRecord::Migration + def change + add_column :projects, :email_recipients, :string, default: '', null: false + add_column :projects, :email_add_committer, :boolean, default: true, null: false + add_column :projects, :email_all_broken_builds, :boolean, default: true, null: false + end +end diff --git a/db/ci/migrate/20140130121538_rename_project_fields.rb b/db/ci/migrate/20140130121538_rename_project_fields.rb new file mode 100644 index 00000000000..3d7d3e8167e --- /dev/null +++ b/db/ci/migrate/20140130121538_rename_project_fields.rb @@ -0,0 +1,5 @@ +class RenameProjectFields < ActiveRecord::Migration + def change + rename_column :projects, :email_all_broken_builds, :email_only_broken_builds + end +end diff --git a/db/ci/migrate/20140222210357_create_web_hook.rb b/db/ci/migrate/20140222210357_create_web_hook.rb new file mode 100644 index 00000000000..743ad816906 --- /dev/null +++ b/db/ci/migrate/20140222210357_create_web_hook.rb @@ -0,0 +1,9 @@ +class CreateWebHook < ActiveRecord::Migration + def change + create_table :web_hooks do |t| + t.string :url, null: false + t.integer :project_id, null: false + t.timestamps + end + end +end diff --git a/db/ci/migrate/20140506091853_remove_public_key_from_runner.rb b/db/ci/migrate/20140506091853_remove_public_key_from_runner.rb new file mode 100644 index 00000000000..3bf9f036ae8 --- /dev/null +++ b/db/ci/migrate/20140506091853_remove_public_key_from_runner.rb @@ -0,0 +1,5 @@ +class RemovePublicKeyFromRunner < ActiveRecord::Migration + def change + remove_column :runners, :public_key + end +end diff --git a/db/ci/migrate/20140823225019_create_commits_from_builds.rb b/db/ci/migrate/20140823225019_create_commits_from_builds.rb new file mode 100644 index 00000000000..15f84b11511 --- /dev/null +++ b/db/ci/migrate/20140823225019_create_commits_from_builds.rb @@ -0,0 +1,22 @@ +class CreateCommitsFromBuilds < ActiveRecord::Migration + def change + create_table :commits do |t| + t.integer :project_id + t.string :ref, nil: false + t.string :sha, nil: false + t.string :before_sha, nil: false + t.text :push_data, nil: false + + t.timestamps + end + + add_column :builds, :commit_id, :integer + + # Remove commit data from builds + #remove_column :builds, :project_id, :integer + #remove_column :builds, :ref, :string + #remove_column :builds, :sha, :string + #remove_column :builds, :before_sha, :string + #remove_column :builds, :push_data, :text + end +end diff --git a/db/ci/migrate/20140909142245_add_skip_refs_to_projects.rb b/db/ci/migrate/20140909142245_add_skip_refs_to_projects.rb new file mode 100644 index 00000000000..2d7b1a223e2 --- /dev/null +++ b/db/ci/migrate/20140909142245_add_skip_refs_to_projects.rb @@ -0,0 +1,5 @@ +class AddSkipRefsToProjects < ActiveRecord::Migration + def change + add_column :projects, :skip_refs, :string + end +end diff --git a/db/ci/migrate/20141001125939_add_coverage_parser.rb b/db/ci/migrate/20141001125939_add_coverage_parser.rb new file mode 100644 index 00000000000..7ea7d6047a9 --- /dev/null +++ b/db/ci/migrate/20141001125939_add_coverage_parser.rb @@ -0,0 +1,5 @@ +class AddCoverageParser < ActiveRecord::Migration + def change + add_column :projects, :coverage_regex, :string + end +end diff --git a/db/ci/migrate/20141001132129_add_coverage_to_build.rb b/db/ci/migrate/20141001132129_add_coverage_to_build.rb new file mode 100644 index 00000000000..442a3dd28c0 --- /dev/null +++ b/db/ci/migrate/20141001132129_add_coverage_to_build.rb @@ -0,0 +1,5 @@ +class AddCoverageToBuild < ActiveRecord::Migration + def change + add_column :builds, :coverage, :float + end +end diff --git a/db/ci/migrate/20141028162820_add_sha_index_to_build.rb b/db/ci/migrate/20141028162820_add_sha_index_to_build.rb new file mode 100644 index 00000000000..bd2a4de5657 --- /dev/null +++ b/db/ci/migrate/20141028162820_add_sha_index_to_build.rb @@ -0,0 +1,6 @@ +class AddShaIndexToBuild < ActiveRecord::Migration + def change + add_index :builds, :sha + add_index :builds, [:project_id, :sha] + end +end diff --git a/db/ci/migrate/20141031114419_migrate_build_to_commits.rb b/db/ci/migrate/20141031114419_migrate_build_to_commits.rb new file mode 100644 index 00000000000..dc90ec6d15e --- /dev/null +++ b/db/ci/migrate/20141031114419_migrate_build_to_commits.rb @@ -0,0 +1,21 @@ +class MigrateBuildToCommits < ActiveRecord::Migration + def change + execute < Variables > Add Variable`. +**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.** +The variables that are defined in the project settings are send along with the build script to the runner. +The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml. +It is also important that secret's value is hidden in the build log. + +You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners): +1. `$SECRET_VARIABLE` - use it for non-Windows runners +2. `%SECRET_VARIABLE%` - use it for Windows Batch runners diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md new file mode 100644 index 00000000000..84eaf29efd1 --- /dev/null +++ b/doc/ci/docker/README.md @@ -0,0 +1,4 @@ +# Docker integration + ++ [Using Docker Images](using_docker_images.md) ++ [Using Docker Build](using_docker_build.md) \ No newline at end of file diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md new file mode 100644 index 00000000000..702a6c6b587 --- /dev/null +++ b/doc/ci/docker/using_docker_build.md @@ -0,0 +1,112 @@ +# Using Docker Build + +GitLab CI can allows you to use Docker Engine to build and test docker-based projects. + +**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: + +1. create application image, +1. run test against created image, +1. push image to remote registry, +1. deploy server from pushed image + +It's also useful in case when your application already has the `Dockerfile` that can be used to create and test image: +```bash +$ docker build -t my-image dockerfiles/ +$ docker run my-docker-image /script/to/run/tests +$ 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.** + +There are two methods to enable the use of `docker build` and `docker run` during build. + +## 1. 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. + +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 \ + --url http://ci.gitlab.com \ + --token RUNNER_TOKEN \ + --executor shell + --description "My Runner" + ``` + +2. Install Docker on server. + + For more information how to install Docker on different systems checkout the [Supported installations](https://docs.docker.com/installation/). + +3. Add `gitlab-runner` user to `docker` group: + + ```bash + $ sudo usermod -aG docker gitlab-runner + ``` + +4. Verify that `gitlab-runner` has access to Docker: + + ```bash + $ sudo -u gitlab-runner -H docker info + ``` + + You can now verify that everything works by adding `docker info` to `.gitlab-ci.yml`: + ```yaml + before_script: + - docker info + + build_image: + script: + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests + ``` + +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). + +## 2. Use docker-in-docker executor + +Second approach is to use special Docker image with all tools installed (`docker` and `docker-compose`) and run build script in context of that image in privileged mode. +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 command line to use `docker` and `privileged` mode: + + ```bash + $ sudo gitlab-runner register -n \ + --url http://ci.gitlab.com \ + --token RUNNER_TOKEN \ + --executor docker \ + --description "My Docker Runner" \ + --docker-image "gitlab/dind:latest" \ + --docker-privileged + ``` + + The above command will register new Runner to use special [gitlab/dind](https://registry.hub.docker.com/u/gitlab/dind/) image which is provided by GitLab Inc. + The image at the start runs Docker daemon in [docker-in-docker](https://blog.docker.com/2013/09/docker-can-now-run-within-docker/) mode. + +1. You can now use `docker` from build script: + + ```yaml + before_script: + - docker info + + build_image: + script: + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests + ``` + +1. However, by enabling `--docker-privileged` you are effectively disables all security mechanisms of containers and exposing your host to privilege escalation which can lead to container breakout. +For more information you could be interested in checking out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration). + diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md new file mode 100644 index 00000000000..ef449cd45bc --- /dev/null +++ b/doc/ci/docker/using_docker_images.md @@ -0,0 +1,203 @@ +# Using Docker Images +GitLab CI can use [Docker Engine](https://www.docker.com/) to build projects. + +Docker is an open-source project that allows to use predefined images to run applications +in independent "containers" that are run within a single Linux instance. +[Docker Hub](https://registry.hub.docker.com/) have rich database of built images that can be used to build applications. + +Docker when used with GitLab CI runs each build in separate and isolated container using predefined image and always from scratch. +It makes it easier to have simple and reproducible build environment that can also be run on your workstation. +This allows you to test all commands from your shell, rather than having to test them on a CI server. + +### Register Docker runner +To use GitLab Runner with Docker you need to register new runner to use `docker` executor: + +```bash +gitlab-ci-multi-runner register \ + --url "https://ci.gitlab.com/" \ + --registration-token "PROJECT_REGISTRATION_TOKEN" \ + --description "docker-ruby-2.1" \ + --executor "docker" \ + --docker-image ruby:2.1 \ + --docker-postgres latest \ + --docker-mysql latest +``` + +**The registered runner will use `ruby:2.1` image and will run two services (`postgres:latest` and `mysql:latest`) that will be accessible for time of the build.** + +### What is image? +The image is the name of any repository that is present in local Docker Engine or any repository that can be found at [Docker Hub](https://registry.hub.docker.com/). +For more information about the image and Docker Hub please read the [Docker Fundamentals](https://docs.docker.com/introduction/understanding-docker/). + +### What is service? +Service is just another image that is run for time of your build and is linked to your build. This allows you to access the service image during build time. +The service image can run any application, but most common use case is to run some database container, ie.: `mysql`. +It's easier and faster to use existing image, run it as additional container than install `mysql` every time project is built. + +#### How is service linked to the build? +There's good document that describes how Docker linking works: [Linking containers together](https://docs.docker.com/userguide/dockerlinks/). +To summarize: if you add `mysql` as service to your application, the image will be used to create container that is linked to build container. +The service container for MySQL will be accessible under hostname `mysql`. +So, **to access your database service you have to connect to host: `mysql` instead of socket or `localhost`**. + +### How to use other images as services? +You are not limited to have only database services. +You can hand modify `config.toml` to add any image as service found at [Docker Hub](https://registry.hub.docker.com/). +Look for `[runners.docker]` section: +``` +[runners.docker] + image = "ruby:2.1" + services = ["mysql:latest", "postgres:latest"] +``` + +For example you need `wordpress` instance to test some API integration with `Wordpress`. +You can for example use this image: [tutum/wordpress](https://registry.hub.docker.com/u/tutum/wordpress/). +This is image that have fully preconfigured `wordpress` and have `MySQL` server built-in: +``` +[runners.docker] + image = "ruby:2.1" + services = ["mysql:latest", "postgres:latest", "tutum/wordpress:latest"] +``` + +Next time when you run your application the `tutum/wordpress` will be started +and you will have access to it from your build container under hostname: `tutum_wordpress`. + +Alias hostname for the service is made from the image name: +1. Everything after `:` is stripped, +2. '/' is replaced to `_`. + +### Configuring services +Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment. + +GitLab Runner 0.5.0 and up passes all YAML-defined variables to created service containers. + +1. To configure database name for [postgres](https://registry.hub.docker.com/u/library/postgres/) service, +you need to set POSTGRES_DB. + + ```yaml + services: + - postgres + + variables: + POSTGRES_DB: gitlab + ``` + +1. To use [mysql](https://registry.hub.docker.com/u/library/mysql/) service with empty password for time of build, +you need to set MYSQL_ALLOW_EMPTY_PASSWORD. + + ```yaml + services: + - mysql + + variables: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + ``` + +For other possible configuration variables check the +https://registry.hub.docker.com/u/library/mysql/ or https://registry.hub.docker.com/u/library/postgres/ +or README page for any other Docker image. + +**Note: All variables will passed to all service containers. It's not designed to distinguish which variable should go where.** + +### Overwrite image and services +It's possible to overwrite `docker-image` and specify services from `.gitlab-ci.yml`. +If you add to your YAML the `image` and the `services` these parameters +be used instead of the ones that were specified during runner's registration. +``` +image: ruby:2.2 +services: + - postgres:9.3 +before_install: + - bundle install + +test: + script: + - bundle exec rake spec +``` + +It's possible to define image and service per-job: +``` +before_install: + - bundle install + +test:2.1: + image: ruby:2.1 + services: + - postgres:9.3 + script: + - bundle exec rake spec + +test:2.2: + image: ruby:2.2 + services: + - postgres:9.4 + script: + - bundle exec rake spec +``` + +#### How to enable overwriting? +To enable overwriting you have to **enable it first** (it's disabled by default for security reasons). +You can do that by hand modifying runner configuration: `config.toml`. +Please go to section where is `[runners.docker]` definition for your runner. +Add `allowed_images` and `allowed_services` to specify what images are allowed to be picked from `.gitlab-ci.yml`: +``` +[runners.docker] + image = "ruby:2.1" + allowed_images = ["ruby:*", "python:*"] + allowed_services = ["mysql:*", "redis:*"] +``` +This enables you to use in your `.gitlab-ci.yml` any image that matches above wildcards. +You will be able to pick only `ruby` and `python` images. +The same rule can be applied to limit services. + +If you are courageous enough, you can make it fully open and accept everything: +``` +[runners.docker] + image = "ruby:2.1" + allowed_images = ["*", "*/*"] + allowed_services = ["*", "*/*"] +``` + +**It the feature is not enabled, or image isn't allowed the error message will be put into the build log.** + +### How Docker integration works +1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`. +1. Create cache container to store all volumes as defined in `config.toml` and `Dockerfile` of build image (`ruby:2.1` as in above example). +1. Create build container and link any service container to build container. +1. Start build container and send build script to the container. +1. Run build script. +1. Checkout code in: `/builds/group-name/project-name/`. +1. Run any step defined in `.gitlab-ci.yml`. +1. Check exit status of build script. +1. Remove build container and all created service containers. + +### How to debug a build locally +1. Create a file with build script: +```bash +$ cat < build_script +git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner +cd /builds/gitlab-org/gitlab-ci-multi-runner +make <- or any other build step +EOF +``` + +1. Create service containers: +``` +$ docker run -d -n service-mysql mysql:latest +$ docker run -d -n service-postgres postgres:latest +``` +This will create two service containers (MySQL and PostgreSQL). + +1. Create a build container and execute script in its context: +``` +$ cat build_script | docker run -n build -i -l mysql:service-mysql -l postgres:service-postgres ruby:2.1 /bin/bash +``` +This will create build container that has two service containers linked. +The build_script is piped using STDIN to bash interpreter which executes the build script in container. + +1. At the end remove all containers: +``` +docker rm -f -v build service-mysql service-postgres +``` +This will forcefully (the `-f` switch) remove build container and service containers +and all volumes (the `-v` switch) that were created with the container creation. diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md new file mode 100644 index 00000000000..e0b9fa0e25d --- /dev/null +++ b/doc/ci/examples/README.md @@ -0,0 +1,5 @@ +# Build script examples + ++ [Test and deploy Ruby Application to Heroku](test-and-deploy-ruby-application-to-heroku.md) ++ [Test and deploy Python Application to Heroku](test-and-deploy-python-application-to-heroku.md) ++ [Test Clojure applications](examples/test-clojure-application.md) diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md new file mode 100644 index 00000000000..859adf5f465 --- /dev/null +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -0,0 +1,72 @@ +## Test and Deploy a python application +This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application. + +You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://ci.gitlab.com/projects/4080). + +### Configure project +This is what the `.gitlab-ci.yml` file looks like for this project: +```yaml +test: + script: + # this configures django application to use attached postgres database that is run on `postgres` host + - export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app + - apt-get update -qy + - apt-get install -y python-dev python-pip + - pip install -r requirements.txt + - python manage.py test + +staging: + type: deploy + script: + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-python-test-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master + +production: + type: deploy + script: + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-python-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY + only: + - tags +``` + +This project has three jobs: +1. `test` - used to test rails application, +2. `staging` - used to automatically deploy staging environment every push to `master` branch +3. `production` - used to automatically deploy production environmnet for every created tag + +### Store API keys +You'll need to create two variables in `Project > Variables`: +1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, +2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. + +Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account). + +### Create Heroku application +For each of your environments, you'll need to create a new Heroku application. +You can do this through the [Dashboard](https://dashboard.heroku.com/). + +### Create runner +First install [Docker Engine](https://docs.docker.com/installation/). +To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner). +You can use public runners available on `ci.gitlab.com`, but you can register your own: +``` +gitlab-ci-multi-runner register \ + --non-interactive \ + --url "https://ci.gitlab.com/" \ + --registration-token "PROJECT_REGISTRATION_TOKEN" \ + --description "python-3.2" \ + --executor "docker" \ + --docker-image python:3.2 \ + --docker-postgres latest +``` + +With the command above, you create a runner that uses [python:3.2](https://registry.hub.docker.com/u/library/python/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. + +To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md new file mode 100644 index 00000000000..a1265ae8833 --- /dev/null +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -0,0 +1,67 @@ +## Test and Deploy a ruby application +This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application. + +You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://ci.gitlab.com/projects/4050). + +### Configure project +This is what the `.gitlab-ci.yml` file looks like for this project: +```yaml +test: + script: + - apt-get update -qy + - apt-get install -y nodejs + - bundle install --path /cache + - bundle exec rake db:create RAILS_ENV=test + - bundle exec rake test + +staging: + type: deploy + script: + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-ruby-test-staging --api-key=$HEROKU_STAGING_API_KEY + only: + - master + +production: + type: deploy + script: + - gem install dpl + - dpl --provider=heroku --app=gitlab-ci-ruby-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY + only: + - tags +``` + +This project has three jobs: +1. `test` - used to test rails application, +2. `staging` - used to automatically deploy staging environment every push to `master` branch +3. `production` - used to automatically deploy production environmnet for every created tag + +### Store API keys +You'll need to create two variables in `Project > Variables`: +1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app, +2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. + +Find your Heroku API key in [Manage Account](https://dashboard.heroku.com/account). + +### Create Heroku application +For each of your environments, you'll need to create a new Heroku application. +You can do this through the [Dashboard](https://dashboard.heroku.com/). + +### Create runner +First install [Docker Engine](https://docs.docker.com/installation/). +To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner). +You can use public runners available on `ci.gitlab.com`, but you can register your own: +``` +gitlab-ci-multi-runner register \ + --non-interactive \ + --url "https://ci.gitlab.com/" \ + --registration-token "PROJECT_REGISTRATION_TOKEN" \ + --description "ruby-2.1" \ + --executor "docker" \ + --docker-image ruby:2.1 \ + --docker-postgres latest +``` + +With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. + +To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. \ No newline at end of file diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md new file mode 100644 index 00000000000..6c6faf8f928 --- /dev/null +++ b/doc/ci/examples/test-clojure-application.md @@ -0,0 +1,35 @@ +## Test Clojure applications + +This example will guide you how to run tests in your Clojure application. + +You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://ci.gitlab.com/projects/6306). + +### Configure project + +This is what the `.gitlab-ci.yml` file looks like for this project: + +```yaml +variables: + POSTGRES_DB: sample-test + DATABASE_URL: "postgresql://postgres@postgres:5432/sample-test" + +before_script: + - apt-get update -y + - apt-get install default-jre postgresql-client -y + - wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein + - chmod a+x lein + - export LEIN_ROOT=1 + - PATH=$PATH:. + - lein deps + - lein migratus migrate + +test: + script: + - lein test +``` + +In before script we install JRE and [Leiningen](http://leiningen.org/). +Sample project uses [migratus](https://github.com/yogthos/migratus) library to manage database migrations. +So we added database migration as last step of `before_script` section + +You can use public runners available on `ci.gitlab.com` for testing your application with such configuration. diff --git a/doc/ci/install/README.md b/doc/ci/install/README.md new file mode 100644 index 00000000000..8cbc858458c --- /dev/null +++ b/doc/ci/install/README.md @@ -0,0 +1,276 @@ +# Select Version to Install +Make sure you view this installation guide from the branch (version) of GitLab CI you would like to install. In most cases +this should be the highest numbered stable branch (example shown below). + +![capture](http://i.imgur.com/fmdlXxa.png) + +If this is unclear check the [GitLab Blog](http://blog.gitlab.org/) for installation guide links by version. + +## GitLab CI 7.12 requires GitLab 7.12 or newer + +other [requirements](requirements.md) + +# Setup: + +## 1. Packages / Dependencies + +`sudo` is not installed on Debian by default. Make sure your system is +up-to-date and install it. + + sudo apt-get update + sudo apt-get upgrade + +**Note:** +During this installation some files will need to be edited manually. If +you are familiar with vim set it as default editor with the commands +below. If you are not familiar with vim please skip this and keep using +the default editor. + + # Install vim + sudo apt-get install vim + sudo update-alternatives --set editor /usr/bin/vim.basic + +Install the required packages: + + sudo apt-get install wget curl gcc checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libreadline6-dev libc6-dev libssl-dev libmysql++-dev make build-essential zlib1g-dev openssh-server git-core libyaml-dev postfix libpq-dev libicu-dev openssl nodejs + sudo apt-get install redis-server + +# 2. Ruby + +Download Ruby and compile it: + + mkdir /tmp/ruby && cd /tmp/ruby + curl --progress http://cache.ruby-lang.org/pub/ruby/ruby-2.1.6.tar.bz2 | tar xj + cd ruby-2.1.6/ + ./configure --disable-install-rdoc + make + sudo make install + +Install the Bundler Gem: + + sudo gem install bundler --no-ri --no-rdoc + + +## 3. GitLab CI user: + + sudo adduser --disabled-login --gecos 'GitLab CI' gitlab_ci + + +## 4. Prepare the database + +We recommend PostgreSQL but you can also use MySQL + +### MySQL + + # Install the database packages + sudo apt-get install mysql-server mysql-client libmysqlclient-dev + + # Login to MySQL + $ mysql -u root -p + + # Create the GitLab CI database + mysql> CREATE DATABASE IF NOT EXISTS `gitlab_ci_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`; + + # Create the MySQL User change $password to a real password + mysql> CREATE USER 'gitlab_ci'@'localhost' IDENTIFIED BY '$password'; + + # Grant proper permissions to the MySQL User + mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlab_ci_production`.* TO 'gitlab_ci'@'localhost'; + + # Logout MYSQL + mysql> exit; + +### PostgreSQL + + # Install the database packages + sudo apt-get install -y postgresql-9.1 libpq-dev + + # Login to PostgreSQL + sudo -u postgres psql -d template1 + + # Create a user for GitLab CI. We do not specify a password because we are using peer authentication. + template1=# CREATE USER gitlab_ci; + + # Create the GitLab CI production database & grant all privileges on database + template1=# CREATE DATABASE gitlab_ci_production OWNER gitlab_ci; + + # Quit the database session + template1=# \q + + # Try connecting to the new database with the new user + sudo -u gitlab_ci -H psql -d gitlab_ci_production + +## 5. Get code + + cd /home/gitlab_ci/ + + sudo -u gitlab_ci -H git clone https://gitlab.com/gitlab-org/gitlab-ci.git + + cd gitlab-ci + + sudo -u gitlab_ci -H git checkout 7-12-stable + +## 6. Setup application + + # Edit application settings + # Production + sudo -u gitlab_ci -H cp config/application.yml.example config/application.yml + sudo -u gitlab_ci -H editor config/application.yml + # Development + #sudo -u gitlab_ci -H cp config/application.yml.example.development config/application.yml + + # Copy the example secrets file + sudo -u gitlab_ci -H cp config/secrets.yml.example config/secrets.yml + sudo -u gitlab_ci -H chmod 0600 config/secrets.yml + + # Edit web server settings + sudo -u gitlab_ci -H cp config/unicorn.rb.example config/unicorn.rb + sudo -u gitlab_ci -H editor config/unicorn.rb + + # Create socket and pid directories + sudo -u gitlab_ci -H mkdir -p tmp/sockets/ + sudo chmod -R u+rwX tmp/sockets/ + sudo -u gitlab_ci -H mkdir -p tmp/pids/ + sudo chmod -R u+rwX tmp/pids/ + + # Change the permissions of the directory where build traces are stored + sudo chmod -R u+rwX builds/ + +### Install gems + + # For MySQL (note, the option says "without ... postgres") + sudo -u gitlab_ci -H bundle install --without development test postgres --deployment + + # Or for PostgreSQL (note, the option says "without ... mysql") + sudo -u gitlab_ci -H bundle install --without development test mysql --deployment + +### Setup db + + # mysql + sudo -u gitlab_ci -H cp config/database.yml.mysql config/database.yml + + # postgres + sudo -u gitlab_ci -H cp config/database.yml.postgresql config/database.yml + + # Edit user/password (not necessary with default Postgres setup) + sudo -u gitlab_ci -H editor config/database.yml + + # Setup tables + sudo -u gitlab_ci -H bundle exec rake setup RAILS_ENV=production + + # Setup schedules + sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production + +### Secure secrets.yml + +The `secrets.yml` file stores encryption keys for sessions and secure variables. +Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups. +Otherwise your secrets are exposed if one of your backups is compromised. + +## 8. Install Init Script + +Copy the init script (will be /etc/init.d/gitlab_ci): + + sudo cp /home/gitlab_ci/gitlab-ci/lib/support/init.d/gitlab_ci /etc/init.d/gitlab_ci + +Make GitLab CI start on boot: + + sudo update-rc.d gitlab_ci defaults 21 + + +Start your GitLab CI instance: + + sudo service gitlab_ci start + # or + sudo /etc/init.d/gitlab_ci start + + +# 8. Nginx + + +## Installation + + sudo apt-get install nginx + +## Site Configuration + +Download an example site config: + + sudo cp /home/gitlab_ci/gitlab-ci/lib/support/nginx/gitlab_ci /etc/nginx/sites-available/gitlab_ci + sudo ln -s /etc/nginx/sites-available/gitlab_ci /etc/nginx/sites-enabled/gitlab_ci + +Make sure to edit the config file to match your setup: + + # Change **YOUR_SERVER_IP** and **YOUR_SERVER_FQDN** + # to the IP address and fully-qualified domain name + # of your host serving GitLab CI + sudo editor /etc/nginx/sites-enabled/gitlab_ci + +## Check your configuration + + sudo nginx -t + +## Start nginx + + sudo /etc/init.d/nginx start + +# 9. GitLab OAuth2 application + + +Go to the admin area of GitLab, to the `Application` section. Create an application for the GitLab CI +For callback URL use: `http://ci.example.com/user_sessions/callback` if you use http, or `https://ci.example.com/user_sessions/callback` if you use https. + +When `app_id` and `app_secret` are generated add them to the GitLab CI config: + +``` +production: + gitlab_server: + url: 'http://gitlab.example.com' + app_id: XXXXXX + app_secret: XXXXXX + +``` + + +# 10. Runners + + +Now you need Runners to process your builds. +Checkout the [Gitlab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it + +# Done! + + +Visit YOUR_SERVER for your first GitLab CI login. +You will be asked to authorize with your GitLab credentials. + +**Enjoy!** + +## Advanced settings + +### SMTP email settings + +If you want to use SMTP do next: + + # Copy config file + sudo -u gitlab_ci -H cp config/initializers/smtp_settings.rb.sample config/initializers/smtp_settings.rb + + # Edit it with your settings + sudo -u gitlab_ci -H editor config/initializers/smtp_settings.rb + +Restart application + +### Custom Redis Connection + +If you'd like Resque to connect to a Redis server on a non-standard port or on +a different host, you can configure its connection string via the +`config/resque.yml` file. + + # example + production: redis://redis.example.tld:6379 + +If you want to connect the Redis server via socket, then use the "unix:" URL scheme +and the path to the Redis socket file in the `config/resque.yml` file. + + # example + production: unix:/path/to/redis/socket diff --git a/doc/ci/install/requirements.md b/doc/ci/install/requirements.md new file mode 100644 index 00000000000..6c12607c3f8 --- /dev/null +++ b/doc/ci/install/requirements.md @@ -0,0 +1,61 @@ +# Requirements + +## Operating Systems + +### Supported Unix distributions + +- Ubuntu +- Debian +- CentOS +- Red Hat Enterprise Linux (please use the CentOS packages and instructions) +- Scientific Linux (please use the CentOS packages and instructions) +- Oracle Linux (please use the CentOS packages and instructions) + +For the installations options please see [the installation page on the GitLab website](https://about.gitlab.com/installation/). + +### Unsupported Unix distributions + +- OS X +- Arch Linux +- Fedora +- Gentoo +- FreeBSD + +### Non-Unix operating systems such as Windows + +GitLab CI is developed for Unix operating systems. +GitLab CI does **not** run on Windows and we have no plans of supporting it in the near future. +Please consider using a virtual machine to run GitLab CI. + +## Ruby versions + +GitLab requires Ruby (MRI) 2.0 or 2.1 +You will have to use the standard MRI implementation of Ruby. +We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab CI needs several Gems that have native extensions. + + +### Memory + +You need at least 1GB of addressable memory (RAM + swap) to install and use GitLab CI! + +## Unicorn Workers + +It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests. + +For most instances we recommend using: CPU cores + 1 = unicorn workers. +So for a machine with 2 cores, 3 unicorn workers is ideal. + +For all machines that have 1GB and up we recommend a minimum of three unicorn workers. +If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping. +With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check). +If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping. + +To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). + +## Supported web browsers + +- Chrome (Latest stable version) +- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) +- Safari 7+ (known problem: required fields in html5 do not work) +- Opera (Latest released version) +- IE 10+ diff --git a/doc/ci/migration_to_omnibus/README.md b/doc/ci/migration_to_omnibus/README.md new file mode 100644 index 00000000000..ae46f59a7bb --- /dev/null +++ b/doc/ci/migration_to_omnibus/README.md @@ -0,0 +1,29 @@ +## Migrating to packaged CI + +Since version 5.1 GitLab CI is shipping as part of the GitLab omnibus package. This guide describes how to migrate GitLab CI from a source installation to an Omnibus package. + +### 1. Update GitLab + +Update GitLab CI manually to the version that you will install using the omnibus package (at least 7.11). Follow the update [manual for installation from sourse](update/README.md) + +### 2. Backup + +``` +sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production +``` + +This command will create a backup file in the tmp folder +(`/home/gitlab_ci/gitlab_ci/tmp/backups/*_gitlab_ci_backup.tar.gz`). You can read more in the [GitLab CI backup/restore documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md) + +### 2. Install a packaged GitLab CI + +This process is described in the [instruction for enabling GitLab CI](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/gitlab-ci/README.md) + +### 4. Restore backup + +Put backup file to directory `/var/opt/gitlab/backups`. +Run the restore command: + +``` +sudo gitlab-ci-rake backup:restore +``` diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md new file mode 100644 index 00000000000..d77061c14cd --- /dev/null +++ b/doc/ci/permissions/README.md @@ -0,0 +1,24 @@ +# Users Permissions + +GitLab CI relies on user's role on the GitLab. There are three permissions levels on GitLab CI: admin, master, developer, other. + +Admin user can perform any actions on GitLab CI in scope of instance and project. Also user with admin permission can use admin interface. + + + + +| Action | Guest, Reporter | Developer | Master | Admin | +|---------------------------------------|-----------------|-------------|----------|--------| +| See commits and builds | ✓ | ✓ | ✓ | ✓ | +| Retry or cancel build | | ✓ | ✓ | ✓ | +| Remove project | | | ✓ | ✓ | +| Create project | | | ✓ | ✓ | +| Change project configuration | | | ✓ | ✓ | +| Add specific runners | | | ✓ | ✓ | +| Add shared runners | | | | ✓ | +| See events in the system | | | | ✓ | +| Admin interface | | | | ✓ | + + + + diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md new file mode 100644 index 00000000000..3b9156f7409 --- /dev/null +++ b/doc/ci/quick_start/README.md @@ -0,0 +1,119 @@ +# Quick Start + +To start building projects with GitLab CI a few steps needs to be done. + +## 1. Install GitLab and CI + +First you need to have a working GitLab and GitLab CI instance. + +You can omit this step if you use [GitLab.com](http://GitLab.com/). + +## 2. Create repository on GitLab + +Once you login on your GitLab add a new repository where you will store your source code. +Push your application to that repository. + +## 3. Add project to CI + +The next part is to login to GitLab CI. +Point your browser to the URL you have set GitLab CI or use [ci.gitlab.com](http://ci.gitlab.com/) that is linked to [GitLab.com](http://GitLab.com/). + +On the first screen you will see a list of GitLab's projects that you have access to: + +![Projects](projects.png) + +Click **Add Project to CI**. +This will create project in CI and authorize GitLab CI to fetch sources from GitLab. + +> GitLab CI creates unique token that is used to configure GitLab CI service in GitLab. +> This token allows to access GitLab's repository and configures GitLab to trigger GitLab CI webhook on **Push events** and **Tag push events**. +> You can see that token by going to Project's Settings > Services > GitLab CI. +> You will see there token, the same token is assigned in GitLab CI settings of project. + +## 4. Create project's configuration - .gitlab-ci.yml + +The next: You have to define how your project will be built. +GitLab CI uses [YAML](https://en.wikipedia.org/wiki/YAML) file to store build configuration. +You need to create `.gitlab-ci.yml` in root directory of your repository: + +```yaml +before_script: + - bundle install + +rspec: + script: + - bundle exec rspec + +rubocop: + script: + - bundle exec rubocop +``` + +This is the simplest possible build configuration that will work for most Ruby applications: +1. Define two jobs `rspec` and `rubocop` with two different commands to be executed. +1. Before every job execute commands defined by `before_script`. + +The `.gitlab-ci.yml` defines set of jobs with constrains how and when they should be run. +The jobs are defined as top-level elements with name and always have to contain the `script`. +Jobs are used to create builds, which are then picked by [runners](../runners/README.md) and executed within environment of the runner. +What is important that each job is run independently from each other. + +For more information and complete `.gitlab-ci.yml` syntax, please check the [Configuring project (.gitlab-ci.yml)](../yaml/README.md). + +## 5. Add file and push .gitlab-ci.yml to repository + +Once you created `.gitlab-ci.yml` you should add it to git repository and push it to GitLab. + +```bash +git add .gitlab-ci.yml +git commit +git push origin master +``` + +If you refresh the project's page on GitLab CI you will notice a one new commit: + +![](new_commit.png) + +However the commit has status **pending** which means that commit was not yet picked by runner. + +## 6. Configure runner + +In GitLab CI, Runners run your builds. +A runner is a machine (can be virtual, bare-metal or VPS) that picks up builds through the coordinator API of GitLab CI. + +A runner can be specific to a certain project or serve any project in GitLab CI. +A runner that serves all projects is called a shared runner. +More information about different runner types can be found in [Configuring runner](../runners/README.md). + +To check if you have runners assigned to your project go to **Runners**. You will find there information how to setup project specific runner: + +1. Install GitLab Runner software. Checkout the [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner) section to install it. +1. Specify following URL during runner setup: https://ci.gitlab.com/ +1. Use the following registration token during setup: TOKEN + +If you do it correctly your runner should be shown under **Runners activated for this project**: + +![](runners_activated.png) + +### Shared runners + +If you use [ci.gitlab.com](http://ci.gitlab.com/) you can use **Shared runners** provided by GitLab Inc. +These are special virtual machines that are run on GitLab's infrastructure that can build any project. +To enable **Shared runners** you have to go to **Runners** and click **Enable shared runners** for this project. + +## 7. Check status of commit + +If everything went OK and you go to commit, the status of the commit should change from **pending** to either **running**, **success** or **failed**. + +![](commit_status.png) + +You can click **Build ID** to view build log for specific job. + +## 8. Congratulations! + +You managed to build your first project using GitLab CI. +You may need to tune your `.gitlab-ci.yml` file to implement build plan for your project. +A few examples how it can be done you can find on [Examples](../examples/README.md) page. + +GitLab CI also offers **the Lint** tool to verify validity of your `.gitlab-ci.yml` which can be useful to troubleshoot potential problems. +The Lint is available from project's settings or by adding `/lint` to GitLab CI url. diff --git a/doc/ci/quick_start/build_status.png b/doc/ci/quick_start/build_status.png new file mode 100644 index 0000000000000000000000000000000000000000..333259e6acd301b579ebb7b7682b1df1e635a61a GIT binary patch literal 62140 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�n!5^ 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 literal 0 HcmV?d00001 diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/commit_status.png new file mode 100644 index 0000000000000000000000000000000000000000..725b79e6f91d1de8e8240c28f911b2753d92222a GIT binary patch literal 33492 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 literal 0 HcmV?d00001 diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/new_commit.png new file mode 100644 index 0000000000000000000000000000000000000000..3839e893c177ee81865b6791d2b81b664a4dcfc0 GIT binary patch literal 47527 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

    &I#>0_{^-L0SDa6;E-#{Wt}}|6@wPdcqQ{^ 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 Z1Sz8|r}y)&xA_A6i3v#yR`Bco`G1c#sD1zd literal 0 HcmV?d00001 diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/runners_activated.png new file mode 100644 index 0000000000000000000000000000000000000000..c934bd12f41dc17d10eb131b233f630b18fc1761 GIT binary patch literal 60769 zcmeFZWl&t(7C#t*ySux)I|Oaq-Q6L$ySuy7xNC6tpp6qC!5xBIfZ^Wz?)$&0`8rcI zAEv9is?XZX&*|Okx7MzXQc{pYgu{dT^yw2KKw4bo(%Vqm}2JB;3=7#KVA!Et-fzqT=iMb8kBtPcR^qtGB#h?z`@C( z6Z3Laa&zI$4nM8&O+ZReirgcR=zi-4ei{}6GmF6hBZM*g3D&qkdWKBmj+Qw+{84om4@fD7?=70ukg3R z7K&DBsR4FoZAGbp@&36_KqL%HloDa+AbCp+{sffohU5K4l~euw8zCsN*`xgpe#qb< z!?dB9lLAms{lH=*g+WI@UXHpY`wg{H{*eWzfeGh_o2VP zzxN!yzxTxi-GlF*i6DKV@UZ?BYm0{baUb%m)wEo-CKJL?} zPXeBNA0O?_U5!aR?d=>~_&f#4{^sEO`1}`{nT+Ib7FSzAGA(%}5-~?-a}rJ_4ki{d zAvh8e5&>s33qBQb$^VFdTnUm{xw<;>F*AF3crbaeGdVh2GPCmX@-nlqF|)BTesD0l zcsaNldont>kpH9PUp?aHE~d`bPOjFD4kUl+H8yc{a}^{b`^(UOU;p@N?rHshEIGLR zC#;VEng3d0W@Tbw{;%#2QGvgxd`i}y=5|`**7oKOE+008ICyvk{^tL`t^6O0|0AjW zKay-5|10@FEB}!cVE!wB{|V@yZ2e9BNS6?t0P}xSF9c`SB0llylgKB4xTu;Z*jW#< zQtC?2`+Kk30K-?0?9Dxs(9t{m$S~deGCN%kmY-kh0!*j|3`uWP$z8zn;Bk9UtKAoE zya#mUDbR1BY8CbriU?z`35zhy3cTMV z!!y9ZJ@T@5&Gxwn9p^}h_NpKE~S)&ovW|56%~5BUDWDrc09W<)6{4Z`^KL=&MjCVE`0(H!PD4*!9 z>%c#J{(HTQ_QTDE79$?*e<@l37s9_|`dDX?hiypCFM}MKF!?LWza(U`PyZ7$OOUlp zVA?GQBaQ;rKUrV_+f*aPIXe|2esv%vfNBN{?{F!nqf7t(pP-C1&@#;^HzSa%;)@=a z(A&`>zuOJT>KYDqrom@EXjSx?r#U2t{#S>+@;lyzp-BFWwNik4l4Gz-$_6d>@$=^)|_ZoSQfu=fQK9wA)RSIhs?;Zo6E+R!*{OJNy{y1K`%^u_HJ zpXc;;Ng?)5mYZUtb^sXe1L;=9m`Y#ODb@MIkG_qQAzSb8=AzW)wQUBrB``8_#_Yl@!<>&YAUk2(%d?vAUd1jAe;)QDZBS`L>Dq_Oc)vQu=)e1j*%p}(#?>TQG1vy z67-wXM4S+xHH+?V#2myJ>_>V(QdJi}&21-+-DC=Cf`bU`q5ENO>E-dBz~1VNUsiA6 z8!MrrI-!juq@-Z!fu2x+`&Ne}J|m(hRH(D+&CSQ(p*4jgUhKF-^_`B}%iR&iUQSHU zb)BTpuwz5%kV6bmMWc*n&>ntHU0wUKGP#;Lr^=U3yLd#SooIz{)E|2dhU-dS74@bc&j($eTUZr1fz^vR zhAFrDJXrqZV8B^vDQ8Lk5fXa*ma?O(VsRBqpvm`>IZLO zgE58ahUl|kd&{lfUDVEX5;SM4EARRUaAr%tu{FN+V@@o{nvrT@`$J8fB0oZ_#7{A& zZR_2edfJ+^AxZB{o3Zj7nzG^;Zitc>rG#W`fj|wvOIMOs{9w8i$E_j8Wv={5;R@6; zv%eKzY}~wU_-0kFOyDI0mQeuFPq-M}b8&*uLImldVW$RTx<3{xqec6=9T>GDzr+s1G@Fq~7 zW$w??pEaI^*@v54xKa(%vV%b*EcknWkW>Afa+7--g0ftL?A2=3HxCJ|Jgw&D-B-i-o3ZNPkJ zI6V-fp3zNhg`p>5eWN-DX1wSLm8Rr!I7!a&6^Dr`_Yex<3M0UTUGZ9^Bp}8cw8Jhe z_|~pYJJcp*-R`VK!>29^U%-8QIJC&NLXB7j{I$}W>)uyOYo&}NB>i=N82Rg}DE%7d z;k50y4_|;yH^`yb$S-ueqPaiP4$2FsF-Gx$`1J<@VO6qzHXK57gpWlD!B|8NTb+%O0zse|N6wi&DSC9q&GFZ-c~mB zuIQi1Q;k~|E3;h>{oP1or+CVD2RRkbkEXfnhvX0E#5)`65`OobrVAI}(wrsCagc$1(ztl<&ED~m)BfXa;}=eKX?>!EDzjy`d!5h==E`7%zDfO zx%pPh^Y^4luI}h9-NdxI1o5Y!dKAk2+{M&29IMR6%jJ73dyibIR`l!fd64R4lI(L! zrKXE;Azm^5$!>u8JW=Z4=-Jm3wo09;p}eNdOYrj{*1h!woLk;*yso|ju4|Sanq!dB z=NsxF%~VGMzOu$I#G8|6Q=B3ktN3l)ZUc8XPR{b*2hq^mZzAUvksKV8`#Nq&ah#M} z&OOebh%4@-WN|P5`Tb!I0;qucgQ?rL^;KMDd+a;7S8fGBPw3EA5Vr)N-aE3Hj-`Y3 zQ;98I>1Y@HEQ+2I_3xARo-TPFa(-ujsFO7xptmd=SJDW^nzOAjJXyB22?F%hFE$^a z>NhbgP>53t5=)BSGbRnj(1mWQk(msl^iR8^JUkw({X@>D^60~tqE#j5PPTua?~M}D zyl1UgZ?E4}auoD_N*`${7MdG^h<_j?yJ?{s4_rJGkdNL)HVCWE1Lj%Y6AsEak}#BY znB<&n!^yizk0>z2S1v$Wq!KWT2XkDK3d~3hHivoYpQ7Z7^vqi6U{Wp%E+%49at;JT z!ZV~gqGNV?-@(;&)LRn%^evanQ!9kX3<)>MtjANTz! z>Z1~`pR#=#5hffJooV}Dphh{#<2S+Y3k(yNow95mZ^ z0_Nf7q0d=YnYd1o!6l#++2M>4)1xjB!Wia&;@8FPt8@aT{r2d?%H8eL<+Sc!d1OgU zQzJuLkxrRJ&;oL8sim_uA}65IhWTKTA3Ob%2lPn~S%1|T3n~3TI&03DaA@p)$>^c< zPP89z$CM;3$%we4SGFK94 zVL6LCWy0y0pT&)7SfLvDDHkw12k~6{1j7Lp&FZ?CHXIi14HSGc1ZK zu~o`mt8OwQ-Nw|2o>N~`Fn)vfMTxN+bB`e(Q zpU~s;-#``zvM%GeB{Ps1H`f@j;`&ZG{&q2WgE2G2kS!T>(o=3%0wkaw&^! zHXJghk?jQ~OOf07wc5GltfDVeR0ZVXSLl0zRogLg7~6@@L`wz`^+hT`ZT8jg=8-@E z2J_=(Xk)$56H0_TZVU`546Fg-e>WPuhygBA#n2uZ*D>}`KKHjY6!h%7u@KU*;1)l}!XiAgZ7IL7FGO~OB(0&Nh` z{dawvDFi_!3Q)~aoCQ{uP(m}(c`u{sePw9SqB(3z#)>AJ(=R$S5dLX_&&5X2eINC} zdj>rO*q$jp%$AU4Oyx_w0&&$KpzV{yt%s(_uV`L1xQYj8&I}1=9^Y7{`VRP6VCVDu z!X8}YjPv`$WyOS8BdL$`w7DEV87{q;jn(ccW7a!xf?pqo49P8oY&x7@Q7*j`{y>Wv zqhX7{*jDLovDXbe)=`MkbFVTNW0HD`PG^5odL|w|)I5GYZXtGPFfyldgJW;Lg40&4 z>wFJHufG_CT|0d$bhNKal2Nw7fuHeyMgcz#OcC9l0siO=>Aa)_P``a z!gp1I-t**BI5fn8*x!%YE#=2!mR&PJf8AZ}X&rk%bZcgZmZ0bNON70rANaPRyV`P1 zOOP0$h!%I4#w`w6%qvPYvBQ4r%gOlXIae;jF^SeMf$b6N@_tgGRaz8_VLH63tV?lk zcVx%K$?!E-C;{L|%&!@L_q2s1nvZ5-y_L{%B(!lu09P%fv|}xK6>~iIh2hV9Lzt>g zKkUWETKn|lsHx4D`d=f&>AN8HtK+%q+q9-H-n=nTYIm+u-(;KNTOM1k%iO?s?2Qkj zxk=y89rmBurS%0gZ}@j+^w@H2v1d(HjWM$~HEH__hWw(0&YUy(45PbD(|tL=Y_a`? zf*eBnftg*#h2KQdUSX_x>umr}i9{X6Ql%lCn2@4!69%W83TIwP{zD;5jGhZ!&pvgK z_1ztq*36R$H#`bwqb{O5=wEB~NVsoza1Xh^<(DfePWFU-JH|cv)&tNWi!azedCT21e}bSz?g~uyYf_pk(-p(F7ouyrag^kN!g_x6SrIORY zrYXydUasdx5XcX-P5yHwW1wyNU4rFA!W#fstK{w>pM|`^0B4!LsrDyQ^N?J9)_bTs zx(BGv8NIDYnH%d${oHl{wjyrPMl|lQ&h7eKrpNux6)Lqn?)<#S(Yy0roKbkZm+}`- z2|5-ta^Bh`%q$RnBFFGEYwDGt>lbkWS~O<~>Eeo)%<-3S&-0fEW3Y`+ZD^$dN;*Zo zm$NOx9YK$@zC=U@zkmIHIt}E3o@iN(l8@E`7RG4FG1?po*9-EgXdx0lP6d#{Lp zbJ(LmA|T*92;)g1XzWUm_%C_!ue7xKYV-Z4$q@vL{z3TvIsPBc$DBOR9wFT7PiHj# zcrFz?sy(dpAkIPIo%9q)p7m95F%>3*iKb`6ZZWAIeZr#)=-ByrmWa(FX|d?hkh*v# zPn7WSBk5;L?mSpeJ?h5R(34J5Sc!Tw(!#2r?x1oA{lmAFqCG1_ zKIm|1X$Y0+vBJ!C$%s( zDwMkB1#TZp$y;;MT7A`Vn1p)gOG6Noqwz8QoFZ)mMRN?8I!}#c`%+Im;3pE=!cLha zCubE(L}m?M$k7y>k2A!NLpPWm0uJ32*w8@R;4Qc7jYno`J0mpisg|Qet#jN9zI1)} zVlaeO8t~_FS9OGCIJAkIiX{rdB0X8-Ql9vesKUHDa;{sN=V35C@H%OS9YqYKgQg#u(ukz3~0Ak%9yNh|1DP%V2+NG75yj9`i)9D zAyGA6qx9yYhGEpd2R7@+H3~gt4ZNE<_1B`MI3c#NV^?qSF-lT$dUO@GJ*$X!A3h~b z?GSi{41)O703apmvF$RM0iYUY@w`S%VoAH*2Z}}9E4FDhF+U~D)22pX5JAfytS9-F zNbTY$atSLeK37>`#>NnxLB$!}l&_n;XKh7PWcC13p=Nxngt_7_JVT><{JSt#ry~i0G+} zu%lJ;X9bMU=%0?+FaGgRdXqi>oyjo1){T{<0zp_oQzs(jh?Cqzdl|Eb(2^?@JXyvh zUTUG-G4@<4P2jqiR@9PTr@AJA#8K2c!B`4#jFd#%r*;9fV2qNFXd|oIaDmA4hWm&F z!6nVJEo%4x=F9VeZe>+9X!pxVFyHI*Pvq1#oNK0KO1cx& z6*+0IXEN^`yWPZkk!SceQ@QBm#W3u?vnOHpckY^C(#@{VlP*~bA|xqmz1`{nwbMYh z72+ddNIN-E1hNr|ulK*sl3uNV&#%nK#x%%-P7G^c5J!pE?R^CLW&5j{*5VQXNF7>z z06rJErQ*Q8@+CSX?xy32+LjGJYS~g41Mq~9roK&8zpz{NB{4NWu9epLg>0!7UQl{k z8@#6kZYj=wv9j!%eS}1iU(>pNkLM^{N?QUm*P)X-+?7RX(;z=h?4eOKaC0sMbmvu?M<5=37as16)zz9ThslBJjRFN7>5=dXivs zZy~nfLc!I&t!RF&DAZ2MR24t(RP+Xt-zr+$dFI*M_5QYS1?RZFu(TTdi0r*s8N;Da zYWytJtd^owv)h`v3Q#tuTt8aQu**rAg1q8%uyIqOqV3z{HqEQC8PvI* z{qX$YXP&fBx5b{IFKl}tNJn<`W5w#lthA&B&8@*ckTLU^#`Li zi^;)A>=FY&;z5xD{JXIjC0`w?ZB#jcg6gy~22pJqp80_8pV7LMy%^9E)nsL@D*d;{ zYueIpeULBTu!?%MX+*1RsmL)ciPy8M8a-Z66W574@{ztf!Q46Oxs?z)+9h3rCvk;{ zCKUB5tX9i5oUTeR-ltc+((oBDyYT>C`Y{efi8l+DN`1Zp6`1;oG~v_qsn;t)jt%=N z{b}xeXkdnv%xqzmJ;aHQw=V4JzK9xE0t-hdXR*2GzPL$uDPp|mgDKaK3+tm#2Weo_ z6hOyw7i(HVnCE2&5^-n{iG-;h3wfw)7JIAdtAy?X3oCz{ooXuLw`%&dzUb5ZVoZ&v zs$7sE#HkAK+bi!h1c6sD3r9b*vUnIWh}G(DPYHC0AsB0cm82PL9K06?>1!b@yU9A@vHk_?c{}RSkC(LV*?Ui7K}C@ zgJPU!z4tfct5vUxNDL6tDjjBym{Asab-R7*!R;0 zGu>>>^v;!^qLZowM~!8_If0oY{?PH~bdoNH%(=k!8dfHP$};?HP+E2ube6_^N}2Le z(r2}l*ZQVg4i)#hks|P`)z;QjhASw%>7HVS&Qt90u798(KXy(mdB?rSb)V zg!TJDGElEGaA$?etj`rGl6e^T1f+WboareuwHFXNgy&j>Juv4;19I+uwF)PZBjm*~ zX3=Dl2r9dn)xkSbn@8h+YC88ezSOkJQ2eS{O;F*dU0RoE5$Hk3N~3o(Gm`Fy!W$S% z9z1g+5VMAH0|CR!$AOz~bJxceDEp!_iIEwG6G^pDi-3*3R){>Y+6w0y+{XN>Cs13_ zVjqvAK5n%G&v%)5Nnf{gwX?N!06MdpDqtG}L%pQ_iN|f#PRqV5$)YC}oT4rOmsfTV zZkJCUe#B-1sIMP#(GsWhU9=%864{{dMVGFnMjF9~Ih;b5Ntv-CRkULJE*(_j7XB$1Qd@&_8#RdH!#T%d!!sNfGTKC$2+t`~VpwvSZXMW_f6 zTGCWNd7q{E%x*;?f3T6sGd%M2UVLjao!a5i+EGfVuNPicI1)w4yVfABxjr`sN|2xh zTu+Oa@UtSFSlN^YuvJS&CFvq(rCKfQlc|d5@D9bm85iBxRHt!y<#2l?kY0*a#yxSGRJyCA--V;c-18X_6*=_{MvDPgzcl zRc6rArRUV@F_290v-_yUR?w*x76(gloZ5AqguQt=64!owGBWAQ1IJoxQf&c`3tZ_P z%5?pOSaA_Pq1jctmS)&9mx{rjx6gj9VP;^+a18@MG-d(yYt*9Lq{1Hc;vZdA>72w< zJDcP-1-#M}s*nj0%4IP^R=yJ(>tsA(!K@}JT9a#94Eeuu(3l3oA;%PoL#SlE}Y~O4k);5@R`|gRvqStGUSv zKm}c-`W@86+5y}euf2q8HgW)b_c;tiZ4g!Q`7K~P8KWi9B9u{~Zo*)nVa!BYkDlJ@ zD-LQ1&pZMwwYp~$&SYSs^!CW>p0jebc_YZ%eS5C z{P`e`48_3rw0^qQL*oIM`43~&^JGUF2iy&C@HQPI-hbg*LH&U*M<2r3> zXGkYYYtKhG)&-*fyfN?a=M3_i`Hv&eAy@`{{vJK-#uN&ae{ zH6{53v6P=gH#d^vXmw-zRwO2PtxnPVBBC4l#7#{#pafWe5>zAVS=>gGAZX6z=+o=Upm67aB7-P!VR_U=+3rEio z%&pe+hVcP2Ke?)(B!xlAQ>hwCo)yYwqJzULGlbFDy{--r?YWu26&GP##!WO+u_7>vgW@p2xe!%YxGuvHVQnb_nt62Ur6616M95(U>DPj9&;Vk%Zzb}=ZB-rXg zDsW`2o}k@^D@-+hE(F1=V_Shl5QKIrx627tKq?(EDQ2bsqJc%mx+(|h&qYL0j9*d0 zXh>iv=)TOuE^c_90I{UkVQC92^2{t8iB3e}ry;fcuIINaLN=GcWhd!Yi~Y>2VebSF zH;Jp?deTdu2}>Nr#RtATfK5Bied!^TE(h1yky>YGkA|>M zG=pejDTeMQ*kO0fj^Z`(7yMPGd{5!!GriE>P3!%ok+;MwAulhQU&xHRW|vkwXm;^=MmUY5;5k`d0UR|P!MVK zz=st#E{BsOu}Xa&(8kbeW2yOV(mftvHwsgj- zmno#$(TW78VX$S{5XTHkQi)>^0`Ei?JF1l?9lONt6rrD~IPv;!)I^ncwF zN+{@7It5O)tT3xPY*P9~ld zXe4vEaY$}T>!vW3+84H>>B12-%>VI>Xa;zJ!3tVX)Y9q6?`tdGdBLbW3p<+Lb=-NXqD(2k9Tl#g-G*iCxV3_-)?j z>#?h|#x#U42Vp1+I@E2%=*L5NPdURVbe4tOA8C=OxdoEZ!Fz1iNP zZnlc4Ab>?mv)19#DsUkCqYWAtmh4>2%C?POMq`haIA_GhZ6Yfvy$)D_;ig!=aWrB% zFQaY_{JFMH5f%qf!?v_0!Ni54C`OR97(We|&gfUOp@GsXs?m1bU2E_!RXmqNI5h*64hKfuE zUIn;xguW+HEx5|ss=vz=lwvoS^ zwxo7r`N*;q+0voFodgt}W-DIkeDj0Kmqqj)kt^VHDLd&z`X0o~O&VjJl+pRYU7{m# zqs6cS+TIpRdRrKjfd{@VPlqAASb(wFRtrJp0TqNlgYCkda~WS7Q2Cf%c0! ziW*4hMFe{`uVWBgq1e>p1YR&RnHUKKq3yZiQDzntXTejh38#@^k<#1VE*z}ofZW-} z2%)=0VdcpVFl0&a)RMMp1qe41)Ko1W`w9AX-g|05^ot64w zv8i&kGLzn9gKT3WjS6SB;3Y`M&pkfm%YCrRh`jbKkvaLs#HcyRimaTd&Vn~oUbj7f z^iBCw+2JoJ#-qfRj5vaMB?fneITu5uO`LM;f*dx!zF*m3LL(v+wWiK4T;_Kvl5V?H znKFYv@P85-#wp+E=@&R`EKwJb|FqYfRd6X|FkVKZSTc|GfB_DtWRmD%VTdG+0u(?O zq7jC61~m*y4+c#tFkymUO@lUkQ=tcs)Xq@~o&fP8X#vkfNQ`#D$ctdV| z2&-lWLiR`KOxQS!+S-9}^{W`77ysK}MG3JN|7V^v1ag+Hv|9xFSnD*sCb)5^;|bqf zTb+HDy#IVZ8G8yPGN9ag!a@FDWDUoUYfV zN$6ejLf?brgV*ATZa-#zMMvz;BmGKmbU_)eN{dg-OYYu!G7Qn&Ab2{vm@R9?i)_Im|ErJ+o;1`CAFla_-~;_GUZqpFu=FgayUNXo9_Im1q7RiW4)6 z5v3nH!xI~2NS z3B$dA!U+%B^5m9a_RbkZm{OYuZ+}`dbc*RM#&Y;Wcd%LS+iF{8VTlENA<_W7R;;$- zkFQ76^e3Feml%CO*oNH-!igsdEqs1hc|ZVku043eoCi?snJS_@tXIPgBYnefyz#`z zc?F4MYpj~^t23X2@WH4fK2AQOx4_m$<@zhe#%Z{H!d|!j=dE6*T zo=%U}=Q)WbejA}u-^0<((ua4BIilAsDqiUA=g?~>r315daWUeY{W$R>5JH}R{AJ|r zwZTK>d$jl&e=2yw_b4Vmgq@{9Jk~SUuDPHpZW-lzUtxWDeGI>3R&Hkbr!xy-1(i)Z z&`H^e?R6U?b)^Hhe78eHYO>PBortj7yT|;>v58B}kw5ZX87w8IVexs-LqpFK^Kbu! z#KGw(k@6I33b&=KolvpM-6DmuD|vFT-u$`dZUkDC2cwxO)V`Ecv{&Iwb~ z)B7oVnJ|*^RfIxGeTeA!H{;nb(vySBE@>>y4o%1630D*If^ddDWmm~6>8;d_RFxxR zo!B=6Fn*B1&_im^LuUa%Xc zZ93E2IvKVs%$wi<~*79jMMpy+F=WE;^p+aD;^NIWGasE#q^P|nOc)z$O) zZHO`B{h1v%h1`&+sZ>d9uNawqi>{ug_-=|DLszuN(lHN+nNYfFtkG4QR(8i@#XlP2 zWAUCix@iJ()&hYX^d_aZ^J5|vnYU^d>=Ug8bt3eb4tk?Vc5*Qq$wX00URRM!Kf+hfdNZ^1zmH7|BQ!<Po)J@9C3cR74b(Kg6B93H?Ifo3B~3MuxiaI?O)8l>rbzX35a(v*OIRFu zwpVTSCxvs-MuQl2QSwq_>}_|qkDZH)cPO$q{6L&}LU6RU&WYCeKRwKS2(d|kW3?0a zbXI`k#~5*M@6Q{S&@3c=i?FYWTh z1*>qUTWUFNdoofG^P#J_Tqn_DQ;n>|bT+A|aVvN4iECThPlYii?*JmlT+rwz2Xxn&7l5SqbbA4|Z>^f2%OK)}rv}2p72%R77!#7rrC6?ZL zoW?9nbq{dd9EN@42ER;9j^{At)?NbhA@}uF=L~+f-O6#aGb>5WTYB=(MK73hR@fIi z?fHIJAb99JQPR;sd}^B~W#WYI$l|J9cAalg)Rti6qdZgNp5RMK`Lb4X2 zih0DL(`av*zE|E=inOoh;Lzc$`e))}d<0+F8K?YSGXpAo=)}ugg|bIx8_|bAh~!gM z4WY7*7TaTl#%Hh9anjVsovb?UMYj~sn_lNJ@tJqN;`4fjLlQbwpUWY~jOz6p_!f3@V`HlPIwzmH%tgP56xP_${h z0gh>H!zi378j8LAmt@Z5`*r2+Zy56WN#j}taT|BI97q@9eoy#L&n=oNg{*HI3fNDD zsG7y=&o9uC3g3rp>y=9wX;G2+D&0gQZm0c7pYYAr+J!yC^J_YQOmNwWV zu1Q-5Cbndn{n|U4I;UrGfxyudNt_8qhYN3tv2lU>0xWC7kxC2CrI>Pf^(A*c&=4Lu z8;eMsBe9-e(@sjyJg>?{&*Y-Gp$G9s+){;`Zi6I{nHNg1wv~pLcI^D>w#cy%aS2#h zqPjw&DF=V#qolZi%82&juZDR>1-ThCY;N1&FHcA2qpRHO!JG?n9YN`QSywU;q0-|< zNlAn#>Ut%h?S7?9Tl9)f!nk}!I1%s6adLIUtqgGf7!YF+Q9=ra0uiFg$SVB5t=)ag z9ll7mam7Jzw0QMqkc}0$_2Y$0?OZvn={Lx}KVGCZl6PlXW^OL7?!fZ#lRME->0&Zj zxln0A6es03)R-gl-Bv#NgdkzMmVGxa9aqx15DBiE5Evb{adbDU>(KkqiB;0Vo=mJ9 zrej;Z%~m$&mnQ!Zn}4klox)V9QXICPiKa3)I&7|`crIaL#@veSqr9DJlNvw)r1}v` zmTM~cM+__jIK=xn7agNdVcK_!b`y=InM9$S8-;OTUCyn(`=qOGX zvfb}7@5sVsUT0^Hqs6Wg3~d4&(CUlgXK6Nq)^d7|h!9_0LQn0r}wy~-sgIE_y z*D#;q3-P`Xrc7TEld_}k*RDWZgsP4mTZNm0hMSS57+1ugi>4UO%PtEY(Svza-PEu? zJ5?hCx8eQkqF7Rx2|TA6v=_6C(@w}gNB50VC$M7C7q2G@2CO*cd3D&D zUpsnQ+c6EJjVi`n;;gu@^97L?YBt(frl-}O#-mJpH^~HEL|s{J19D{Yb?5Ni$H5;m z54)OC1v|PTHqs{|55yCnz{OhVq)G}`Ks4qHciJzrIQV6`4$o)b-QUq^52@Z(06DR2 zd6tF&EW$7lpE7x1T{c}6zPDNTpKOgcSrrrOMv}NK`jf;q)mnB1JVshKDu33_Gj|=W zRybQt*;bf|uY3fKZ6!H-IhEw|RN7n>jUMeVorwAh(3{9qKv;ND7&ir{yXlNEH)mHY z1N_$9%l-f~nq-U&E7)$dTP>MhX3f5)n_qSkW zKPqx{gMsZSEs-s#nB_kjZ>)KB252+kz3ZVp5l+SyXgBY}L+!egEmOX%)_C)96PC7x z*&xLIqQ~b)P3y$uPm}2|1K`QMa2MV0@oSmthW5ZPh)!HVfzqYgO+XgcLDM{6#cB8l$RL)+j&87E1b~RsV05my=DeW4@Uk) z-y>`BGj@XC($k8WuDep&wLs%Dnzcq)z8=`dWpSWTo-AU6FaLw=q&bXO@s8z6v_K41kqB7LXK`BQLGLZOWXz|MYYHdd|dJ;&Qz0k&! zMjnGIGfNv4vjJ92W{ZfztwTvu!X@qkseL0a8R0$kv~mdT6rBs9I$r@eZ$s}L38w8D z!l(L$O@;jK+A78O){Sevq5-;gP)9_Dnt~crJ-Nw5D^y8#lDj2{PsQRqR8Oes+KpGI z^d|oLGY=;*kxJEGMag~iEhy82wr#;p?uhx^^MxYP&Ck-=9B3V(Rhar=Rip4-n)U*`mXAj z3r6&*;_G+~cVI=X-=37Cl&EM5RsRBc8bT-Erd8=_IFQnRA1y06LX5WQ0ju$|X(6gl z%-(J$kV67@p-~E{UQzBUzhnXRN|9W71;#QV($|jr%m5jUB9nNMZ%1XPyEn~8HTS5z z3A1C7f86F*ZE9lk79?4u)i!JYX8Sy2PIKvvai(QAa)R`8@*9*j-tH*l1_mlL2mCb| zGh0|pm?_xG=i)?UH)T(uOWs?(H^MF9h1dk4m(PdZX>c_VC~U-$ZI5m+gT(UveLH+x zC$F|jjm=MHG+LG}Z{<{uZ*DWpNX?A!5q8zT?3@O-?SdB!VlJWdw* zlp1tnTihaxo^FBVE=IR=dRndRo1=$=x5aKHN#6XG$maQ6e2nlpX+cbU3Z^Mm6x0*E z^l(Zx<*7L)*T#MV9()`VIHZ$rWQ;I?H6@m!qLB*{PlF-ZAx}gl_{Aj?M?Q)rq;E0m4 z7AsN#SG0}hVCD?G<(G8jw;pW#y;SJsUDj*#oHq^x4i9xhoglTP;p(s=-jQhUb1Pt8 z14*?^2vtb_p#T6*r{L&iifcM7nrm3m8nLjh2wr0;T`?o)gz(leRH(v;*L*@FetDM)`s-;Bt?H`On<+pkygQqHb|fk+ zq8g@-fPXIb1l1+{QU$53+!uEQ(s*E&6JK3TQEIkTP~p)qSkVfvWACU%Q7PS{f|h^= z<y8KQ2^QA0+4N7>R(Sp@mo%YF4BVjPC_`$977v9}+sjv*(v!SLy z*x}CpbNJTJ3DU!HGMs8__hvo+64H`WxQOp{T> zZTPBgY4i|dA~L3?yXyflFVb!a;)RUUnoc}JZy|W?S)z<*J~1vA%ZG}3=QkglLFN*+ z&_rqMT>R7}q*xqQlo)0%w}7bF>MEE5ogbapP(w0eLA4E1H7wqQzqvxe~bvY|pE6Z{axte5&-% zYLKj{hbeu7+&HzV@&BRpCz49c%{4)ou%x8sa%nviOJ&Z7{@lqYxG#>$_v(Fg;E3hV zPkN?5H?ya6&K-m+{2$vx-z#EIdni$liaGQTnVazdv7vh5(Q-m=4&s2fGgaT)9|^ye zLmpOspCVsXn$$Xw#LVB>t~mAVi`l(r$?EL;^@!E5Y3dU%zt$iXsFc6(WsM>ixTX}; zle}~#R1#fe)hcpd=1(*yE9cc!w{A+IH@wdIFc3~@6@~E zin`aqi#0sq?3+E}i8*0;@JO1)_f*Rnr1tfqBTO<72}jJ<28)`uqE6@6lAx|rZ086^jw|p!+nJ{lmHMZr8_rHtlI$GZ=I6`yAr%3)vASRM|yuef$A`UI0D?flZ=u`PkFjfyq( zmb|+Bw0qguDC#bLIo=M#`2R^tzRN9&b3Ej{Z2FJA|MPU%uKK5tqF&=v@NcPQ=-VX1 z(^~92@LvIiE@6+- zp44L2!v8CIaSQrxXb;FF{*Ny9ADKSecR|OP*$DsNVK1S+8{ELS#mD}2o3Q@Yw+#Op z;QK#u4JvR~_Eal1WXehGW}8Y#;yvw>fvxF1@csusr9yWn-o!Mo8$hq%>x4Y>{vl(S z;mh~t_ET>G#}mDn2J(5Z{$8|*(sASdVw>)DPTkx2I$Avv*M^^}>FW2q)Yfp(mG4&9 zS;)Ageo1&OQ3J_rG2V$&o^0*yQ>(VzW#4R(J4$B~>-!$^J?)jQwlrdQE`;V>a@`R2 z^2HH=^q)uK1pSttaN@*bf3c$Y+*v(CeuQ^Duv(|$2Rq%rG*0Z{%PGOsi%jHT^M5OM zm6N?1VJ7q8iw9?pZi}WK@{0;k#J4OuD=F?5{xU^9u8!VFT>Y;A;mH|^>K)ouSIFtG z{f29J5wMa=Z#pY~<}Xh`$M4|%k3LnGIA>$njTWq)Xt$c+V6@PEAo?(w;iChJzsmvF z<~s|uOYc5L%KOB}Bjj~wLCsB^_5S%4L7tdjx;nCjwIqIQC+rQ%u;X)TX~dA2#zupIGKG0bYf=_4!U55L~|n6?^_+eJa;Ql?Z=Ks%J)c9n=oG& zUu6#Z1Kwh6ZVbk+K8bYw2>O06cQpeu!c!8@Zpz!ACi@M@+l};B*iC0gXHI|EjqdxN zeRD=Scc@p|`$BJ({e%{i$ueGgnI_^%JihooY;fW9-(k87uUep*9e0+m=jx>y~4; z8YuUHGk6P~ioc7FajeH9gN9wOzzS_?rfatQZnW;hFkHwA4)=-YEt7WLSTjUKWr;mS z)>SxZ!cIn}Bm*~9gZkorhR#pjUaZ(k3GPhEFloExR#gDA%G$o#HbCTw>x^OT|5_V* zin5G`wo}N;cY>~kG7}G#7U}s`F8ZIy{hr*2R~7Pw!fJU)jhAUF5IFQkjSf}3pV7ZL zqVE=nu7Kay8036MWiNDUo_Q<;7mfM~ipEQco>SEy288q}EY3x`(MwnI%zUZ8Z((Q0 z27ChZ%YAbBzE*}@OZeeC%Cqh!#c(`BetwvBlY{YwR1NPM^r_FfesD2KpV=$x;J1nH zRPSa}OA4M{5+i?XS%s_Wpj|Ed5>Qv0;9WP9DjJvhF=Ve;&lxq*?rREyf1lA6!X}=pin63%@S4;{y!i7fMv9y}vIvn#YJuf|$E!qqq+MtN8JY!aYvf5q)-IRaku@ z^WjDcjaQ#=b%ir~P$wwb;C#-z%^nly)&;ab-*v0SitwrKze?hoxeSyF1&l&0QC{Qf!sC%EszH9iq7>CXUTq^%fMO0Aw<~8fJS(P>VLLpv7!e$ihv>RZ z!0q5e=$JG@Y*t>2oGz7tfgqKZP+%WsQC>rm6<f)eJq;Hs&AaC%cg!!x=?SMia4Bave+|T6-gD)u z94UzBpwq^kJV`U9f6W~+S-?1UUmf?x-yO`aewie?1-ysw-#c0O;I2e2R z#q%)f*K-#kX*?(Kp_^NAqBF zSR~8V=j88AOw~+w8aOhZI!c4HsV&Ye+9zUn=WUP?;*YLh9s}fX(F>A_sGU@k?H9?D z--Z0h9f$nH^ZC~S6j8x(KH}YMvAin8DZEkl3s}D0EPva6Rnx%KPs@-lwR@-6rK=bV}Q7gXx3#!a)kwAgEzur=Hn<}PcBt5W>ROi)i1LF$d= zdF_g|3pgYwo6i$L!071hb&&C0>@9-AVm<}3 z>dPxa_e&~l zlp+%^tSO?qJ$i+-Hyopc_mF8tbvLiU-*`CTsq*6_Rrfn>TjB0w{RiE#sk}PzC)iZ` z9}J|o4A4fSbGB~nBiA-}d}iyWA(=m$iX=50Hf9z4$C4=aj|vTASw*Gt&YT&gEYr6y z$w@0I9vQGqp_jKD)s$w!T17`{smM|{Bj#9T=8W|%b?Ov8zwN#Z$Pl)JjFr`-oD7(h zq-B2T)|ylE2vHPv?_8(CJAACMdcXeFQqnO_VLkxTAEET#*H&bq-;ACxZZvmt%PZbP zUU-ibyUX?wB(~7RgfQ&=W!ot6+MRRM{uTMr{SL9rUA6OSO3v3tgGM)*ySO1%K*Kmw z(2o7rkMd~mwJ`enD)B3b3d?$W{)1NuNuu}OFF1MdVfqt0o69fI5B()FrJ5!uUc4H1 z60Sk#bRkdG%_xaS$m5!z`QFBR!7+9B3wJZmW0&f-X0JV3UO{r409*&w+`b`3aU=BL z#6dux*cDy9=fqMxB2dlhs)vc_;*;o4TpSIp_DW1H$-G^nJn>cqy^+j{wJI;Qz_p#+-B{k3es=9sLmn%a| zO-_;3Q{pW<0`CXjt>FMoKYx0S=P572Oet*26`rfox%~8K%CPMcojHlui>C5+Vdc|RrFbsS$%I<|O>Kr{{)LqM* zZ6e4p>sI4$jr1HOF;cv-LA8yTDQD*a^(E_y5}Q%r5r$Ox1qE&Gc<()0#q@MY1Aw|E z=Xd=lfvPAqk7)6Maa21-Vng2Bj}H>@Xmi@X0?~e9mWXH(`xqdW=~jxHYk@xC^)+O( zqMud{26mPEyks7jM464|m9&F{2X7{rYy5+>`;pu4N@zRy>gT;Ur$sf!|M{mrw|^13 zwL^9~^tWfde7;Fy*I=ji7|>TP6}_y6f-_iWx$GC}aOQmT*e8V`mI05&TE2#9F!3il(?~WDrf1#ZN zVhb+mq|@A<&}7!X_-~!_8<}0M=X;UNFNH}kFBr_(u4 z{de3<;%{g=RePB7FGh@v`2JOg9jpx}KRfoeV`#CB5C1JnJl^#Ly+V2d*f;Cj7NP<- z`K_$q4ueDgd!Qcao-1R{&G~wdYHSx%RE{(2{+<{(MMk!QBNIa3fUR7Fg0-_T`-V2i z`_!M4Co&Er8%;M&`<8t|YbWA^kGS?)PB&Ufs!%&6E76W#BESDo+1oB7PKj+W9Tno)ymDC-tVFm~niR}L>>_Nc{&qLj^LF3;E ze&?dSZ5@4k8b8|)!zCNdKN$Xst4V};!H^lK`exIW)^5TB9$mQ*C>j9_$gVlvu?`7cS|4I3FNWlsfeLu;Qp{)9oSR}sRWc$Pp*CtFZO)L=(&r5 zpCGA`6Ljo}mDqJdj=LM>IuVi=WD^JNH((V@`pKxO0Kk&UW+7TgkI9IQYLxEaDf|tL8J;qRa}bovFMMf1Z$f^ z?tDOUb7j(A0zfZicMIa-;)h&E;pT8ZbKa&FA}>^7meC3{c?wIXxo9@ z#Ne+=y$Giu^ZI?EL*<|VEZinj(fqs?9Gff=l*4odmAT;a(oJMMjxs4l%`K{BUS;6#in#rbHGAEoO|bsfA4eF)5GXo>#i@y}?9L~k z@YAs+aeg9L3!E7N95NxYNo)zaiJ4M-4S3{A7!?LUDRyO?ioc&D{shXQC77}z^{3>o z9=EVr6^6nh=Y+IaH({g_C>7D-36i(~N&n!oo=O&E32Vm_lHZ06iWU~YviCl38J|vV zCq;rYIp>)Y*-np}R@EPsK96fw7cNtR;xC3Q8e*~fTA@Sgfc_Q-_Bx~tptznJS-blplM^paTi34{SXAwqphhj8LDky2{1o%jI#YV;p?*3g_&cTJK<6%ABiNzbWVoP z3D*WQ0z%A=jdD3tBUastE^2<-6Yyz{FzL}(Hm$9*&dG9d zmI#mNjVZ(=BllCAFV-Q6%!~hvICa^-K!Ao9yFWOuKAY2T7-&<1+^8~hliEr#p&uo3 zyQJWYZ~@h%cDa0&AAl#YcIqPK>uiQ=na*W+Rxg_lja60o6CDb`w_rd1qoFu5j1QxI z4DCznG zy_mKIhz$jPW2V;}8al~JyOH3cQ%j9qX2ljpd}$=g>|jTg(m-U|;Rr=RFxse^?giputh*u#-j#SIV zuivRo+o3KUEsU8(^i@b~egP12z2q7rK?T7ba8oC}bCHmc7O!jZaJQb&5PH=KUVB}k zk4Rp}X~KhXDTj9DFrCrTj+iQOuE?)Yr;I^lI?TzROCdVXYADhPgw@4yK$)vX7X*x=uMV8d2Dp0TpRpnuf#lXBkGEqoQn)SmR zJ&EzHDBz={;8;gQe;cFl3_!ZTj5%*#4{vcE9BEule0f>BU#DGfg z&^3IHd2Ah{+IH65>dlDomS=fjmLkN1?-8Yddhz@McdWV?2BCD6z3AAGiYp+O5LjIL zO{6?3qTl=6eu*6`9*EEjxTG)_!Q~Ti*`}je>XF04EaQ)a{}^>@I9K>Uo4Tv9Lqba& zfa3B3fB#g;em73lKr4^-d9&Ps zqWY*W*vyA@Q77q6&(}EQPERkga0m`smH19jYd)(QP< ztn@DJC`H^M)jepF1dMo$ORpIn9Q)M1>`bykieb0$<4y9HH)!YaX#8J1|61e2rmHy- zu9=W0czglfkSKQ1pG{X9)pEa)OHYesygkvfj`(-hF3*~BIvWv-Tk&<wgYXEm|NdSOyhF9r#B*Hmx-nTm8pknbw|FtPL1*w=qiZ7DaM=gt$kXK(waXj>dl9MspxZr&s_x8kFmoyV#YKBc; zxNN0whi_0cyo}fc&kblH6D_6_B&%c^T-$ZQ<|Un6Upr<<1$YW%H2M*qcq0&=#1mHWI9y4(dXOU%NgPx4enp-OApjK(= zbrX@Zk`G6?RRjCVkA!7GBSw`hZ1YwIK+~KVJjDi%sdl0}y;Ga_D*pi*GQYfEz!g!% z4P=Csyr-GTA0K1Pk5seQ22rJ>m5X&YwyjZ^<|Y3nSyjImABW0b&w-pw^q_LBxbKiK zCQZVRlzuEy<9vX$fM+cf(fA{HXr3f#%zS@Th7ju83{=R6HBr6Fljs?FbVud+6!Xh- z2hqQ9{CKR=Mo?X!!lqe4tMdfIyi7n=n!K%|vLa^e!^6!c8zc|)2ZVMvR>Zcl;re_i zN3AZw+I_MLq4`5gjOy4NPIgbNYA7|ekjMGee&o*fBcjECuDyj_3>-Kqq=8 z=9oOp(iPtyR9&)^wAK_>Myc+$z^~l|cZfoX5DX~2NxqHFegopo(nwc+*nDak@K$a|b28-(s$ z(wMa)on~SqHjc2o|Ekco6qmvnzHjO=GOqkpd1M#4(mXdvG!`>r3P!6HFDm@QoJZjg zExpQXBW)+tt%$~D={v8kZV<9_=CCP!dQGCS1Y^x10+;!srSjbgTGnYUzPOdh3=(3; zQ|LC2?5S;q`g%y-7ttVgvg6#Ip4lQe1}rO4EKB@65~C$o(Aqqj$S<0~fbEm3pLvA}Yx}}u4l-kZh^S$?kHWE?gcI3B5wY<&<(~0ce>n^P_Q(iT` z)PT?gQ;A=rYjd5>#V$9Krn=_);}T^j-&*?v&W%Os66Du(3{ovSq3lNZ>?IsA7xL_B z-L^lrEnx*1Jd(aPxU$DMQ3LVg8R=WdC0K^uNTV5|Y2(rgqmS)vYd4>@MhmV0B2UT7 zm18~HNr*0n;KaA|aYZ#vcqesqKGTHPE-fbfR#0lp#qxp_mbMN|=#5qRXtTDtVvNsV zz$1e}aP+vTW$gkR!>2=n;$TziUwGE+w3eA25*?*nDsV>TS7|QdEEL?!(*6ijRR)(E zS;v9(0f>j)aJ8k-ex@j|4SYxkS|+YwzYq+O14_n)jYv;Ah10XnuSV?+tqIMPaDyNW z{mB)tXP=Z4sSBpX{1mV3tO8@~1#y%lRG*nb5l+KbNj;ShI=VR)Oc9#tT_?2a*qLk5T4rR4L zK;A|;5QMDMPjdSD?57@f5j)ko7GY`mnT;#jtD$sL#21mLTjJ1oxfU8^SzQP~R-&R5 zR8?s+dF##kx9^W4Jx_dHyk3lgRUtp!n;9CHlA;|nBdkHf z;QWo;qd3AIE^B2!lZ+i1p&fV~G%}t1(4{(8;2VlPH}8%8s9JPnsbl{J4kH6b^VC5c z9m&xKiB}<3r&3dN&fT?Ei+~FZDsG8nh8H)q&^=jL85`VE^QFgqXi`}i)^E7 zqF0$p#&a*B(N8@Q2hUXJNBLYZcw8%r#VVnZdw=BNAAS_uF!n3(nrD*e2o2uD#>If8U2At(smKa{FQ z`Q501371og)>@^L=~^WxGP#$1c>cnjtCiov)bJ@qmq1)h9C$_@h;`M)$IiKO04)iLSIL$6(3jQn7|8IdaCgF^I$b~lgWJS`VTtW1uK^4=(XTs>w41JQ>V z)88)B0hRTCx;X|U4~ysW*<1?EDcSr;!W|B8$`71I?^ZOxsb;%7ueoI;ziEl%qrzfT zj0fX|c!4zKX6P9{8;Wnu+!J&El}E-)5x}^eqdJ_3`t4Kh!2wJ=tS2BEg7~&c0nsvZ zBcFN|Xr>QwxIbL&7vNhhXOY20N%ce(-Famu@8nAN?m_;uBbk>pQ@rB|{C(ps(%Ub# zI!$)71s}{COE`Dj=jtT+00Xcer2Sh!Lt_QDndujhPk(}msK~%TB)4WOaIv$>_MYrr zTj-Cm)gVTY1+r@L3Uj;WoW`_qcfPs$-Jhrp280xTbbKEln+R8*5VE^z`Phq#cEGdy84-n+Zdu^|1y|mIM1lgMVU%qRZ{O z5X|PB$)Un&-2M_0e3O8XQmx_?d2a1`fPD{ z^g@W=v@&IaZU@QI?N-tmggsp&RzigYB?*J!@Wd}-eJ(38Y8UHLvj|g*K!9w;VrZm1EY?3EB;1}Z==;N-1bD`n|KgwIv~S4Cmjkq6_x=_lhq$;VcCc2Rt_GE!Ut|J) zJG*V2AZ1m3PP&58+84C@tTkH~OY=2@Woqx0bh4EOo?|L%sx}50zt)OAl%q z_r{;(w#VW5x4TEj?_6=g{r72%X~~!@O)$fH?*AE?#h7c(3r*b(H;6X0ug(6j`{w2B ztH@1)+ByGcYtz zrMRYP`vW_HXYOcW?Y|o+blcvunt!Z)FrVILYJ4ByGx!JCra;_y>mZ)?s|V<>R7*MN zn)Y-CH4O;SSz@6bmixBkZsS7&k3FxdTi$KCFJC92*6SbjV6xI9z_UD8Btt&9r<(d* zTBxo0?o+$_JG^uwV2qwOwJ{o;sT9YFhXK10;4Uq;nIT>gOWWvazM!|bzxfcPKOl8J zMnp9A$>1Xp60<}8fZ8SanwJKa=KbVHI%*aD3IkV0ch5=hiOuA2QSAtdt+x(z%t+4; z90=iI@to?2h!C1m^JOsU_aiXm9FM8O5Z8tMm9zW@QJUzGKRvQn*sk|x8|uBzoYaWL zM`ZIjm7I^sVcFQBIY_K(HUHh{z(PyH62aKALM<&8a=QKbR)$TJfcHbS7p5btmx z^p4@rdi(FACnJE7N0_wWZK;}H$uoxV-^aaEb$A2cQ79d+yXqRAC;z`Lw!BU#=;3lk zQ^=euIMfWouZ(nl&r$I}N7L6}FiAde4%>v*;yWOiyb`huxPYzEj)GN-(4o9ayZ;ovO6D;s-T^XN_I}9J+ z6Q=DBe^2Q%9b(4_=1OhHYh2^Wmw^*}S5Ytddug<#1D=cPl6)5HPzU%SU+x@GGJITk zyk9SZX+=RJIM_jbUYwxZ|M3gvi$H`_*+;~}3pk(_4GM_%fP%l!%h!^M3)WwBZePQ~9e=92xjrIgfItdka6okUjd`!dMF{JJX|Rh@sq{@H(<$RjuOm5_>%6kj|xR=m(s z?419Vo#e(Nil_UB{!5YHHPoQ0@R8^ z$fj`jVD;(|{dgI}!Vv17uqAGxkpidHOWg238&Ksxi(h)cBaeuJW*H^0;ZK8OoJH$%^jS`5)GaX zDOV1?`wU0$;GB*U{yT8+&;9{aT40BGY@FZHNq${x9hvR-L9MDt`M~k=MhA>mFxT07 z5iOD5=sk^v>&xT3)R|n`L<(H~8R^&M3%vE_@y)giZZ?M{-`k{#eyBc}oqdC>nwxvm zoFy)k){W*;* zudl6*qra>%__#Tp=zxHCQn%?N9nA|*c2vu{kNQuDqV^qw+*gWd&#O6epkc%# z{Rye>rd#CY2D((m{Jbk1&1%=I`zsrqv1i1U%V4KQZwOTndw~YSx1o$sX_+Nl!h-Z2 zXaeZlkQ%x2dp>)rTKAn+MtyK0-W}3YWeAwv7y%D_w0q*V`+Ce+{gM> zcKuGX-sRbEACron_sUV2`?V1IDGijapD(+~fN#0zpqhJLK<`Ms9~nIHelEJMDfwv6 zUZ@^N5`4EdP~-$WnVDb0KI=a(c!`kUslX0u$+yA)-^#|ePwL%z@~GwYoJ|81e!4T# z{HIc@QjPZ|X#bS?j#^B1tpqo5Sh@Y83toJr7Zu(Uq#6_s$1h%G)=#Ht z^l5`_yWgLk!`|j?STOy8rr_5HS^PU=^XW5(9XOA#XH)rt-tpa#!f5IqYa7m^(1|#D ziU57?Yzx<`t5*Z?vR3f%$L)wb-If8*d`uJp@LFbbY3 zoN7rKHUs^tHbn7mGwrPNGx)v5Aa9kuVRU|%gze2n|K%yk&|N&J{$4(=;|Y7B*F&e* zWoUKVoJVC1F4x1_*VYqlj{K&h~-<;R91F}_00jzBi{*LQYkK#ul^DE zx9_FnN#_E%PgBAjB3RO|?cjVckOMKgVe}u!B7@~$Q`2)N%wRMZ2Nyg8Gud4wLNZS) z2X?_9WJrfPyq_$C7-~$QLh+;tQs|Y%`}^fD8mQ__p05Z;Vmdb;QW+6xLc7 zr`Wnn*0f|Y!-$Ri>>u|ub8KWvc6x%(tTjX$=0Y$i#z;Ky{#IXfi>${Fe6ZyDrYw!| z2#z1A|150U5#KOMdEkx9tV;WXw1TrcHAI0=&&g>{XTZHP)!LeRc8}t^w$N@Wv1SAu z!SQnX*iXwvIpoR%6P}KJJ!9xnqOAAeeDR{aApEgUIO{vOKm?xK;1$iOzT<8u%)d>a zQ~sle~D9B`Z}rm&P_w( zc{rvM6Yz*`V zYlDRBCGo%D(O)z0yHmZ`LS6pp|7v*8`FEAV zb^(XFA6it@eJ4nlf?Egdt|uy@yR!~M^j#`;^a(qJ#KNxHL=Tbb!*tU<{xa>+mRaUI zq3l%1`_<6`E&G}SpXjCElSKQsw(yvP3|Q4`kq)I(qtsh(c2f@(AzQM3SG(7+kIvt& z9sk;Ae5%BWn@XRiFCwuB(&2#m2vYS{dBl<}xASkHs#l@7oNu`Nc;V=st@t9Hk(0O4 ziVSr8XQ;h3s+09aT2AD9tisjTNf-V{qx0Uf_wYcJavZB28gRXgBz#VtJ3iKPc~W>xSKdKT$eM34|}A&{oCBmlXO4(dQQ91QE4CrzvGy=!|PjFZTnaO{P{)k>^gIJ z!(z}%hX-aJnUG-uPm3Yqy9~WN9-7doJjGZ0M;sp)%4{z3-g%x>@Nprssb zCHB?=yQj&ul+-cY)~raHm}LQIG{^Z1)snEV zVWf8~mh6J-A%5ue-J3=(WhA+^!nzxne5K2X z;UvG-#D(t`jl3q44rkK^LHNX)Jmi7_Hl<8KYT0b4D7iIM>7;o%A-B(3U{sBGOrJ`e z=Pj!r^-oU$vn?hPxLkhKWRM{no1GSz`W3DZEd{?$6kIV(T)jGR+a1g6Je$J~T6II| zhw(3Mav^mip8!bH16Tn|KE1y&03avcvFK$XR8@*N=H+|_@(tdAS1n8XmO7vhDRRod zbs6Vu%ipmd2$Ku=6k=B1KyQ*m;1eZWdSOLVzJ(_<5+G_-)8cC3o7EXk&iFacA{0$k z(c-3>U|hHs?gvr8FsY}Cnl1r6lu<(X$&u^8V{B`YOuHtC{Z)lH^eMZF8g;0zhaent zIMH#qoTtSsfg_9WpFlWQk%RFTASG2c${vO1&}?=AUhb6YgZN*@Ec8K4Fmr(PKX(Q_ zDuH!-Q8UFyM#Haz9b5?RFx(Ry*K-a7Ro+M-#bvp0AcWb#*1Kl1AcdFmRtkC-vCDDi zB&ObNWD<98rX04C{g+4e>r80w32xyIEl^xelNhN9)_|d|z|0Xp8)_wID!ksIqvyR} z=e(Z%PT~0*S1B`@c-`5Ea^CrBG=cd$plb_2aKdYrC}<|7S#19wUXZHtn~W8K|76}- z{n!R>Y#m+*+=aj5!~+26K9zrPC;S&xorwt*x?SGBhw?eEMwA>06_m}!DNLl6mP5fp z>ooPPK3BlfC56dRm9VWV)lV)gg|_m!;Y?ch1wukv+)?N6+PU!%5{9uV*iV?;+$K0h zX(z>$Zmh6ZUzP-Xk@nrD`uw72QmSLy0@EWSTvYLM0dOiQ>5t1&aZ*e}neboO8T)h0 zF^2n&y;GfSNVgW4qC=K*8kvwje&Cy%Ur49zV0L?jK$q-E6wrU3Bdsg6o3Tpwt4}UI zIC50v1RNJAXriKOxJDb;n~cmCg3*qQ{F!;Muxac?un~4Jgy7NjK!nSb>NIU?SQZ^` zj09{A3DxK&Mf}8Z;nY%9EkS50=qd$<1z75qQHj2(Ga9Fsvm&xqwIxt<$zH?1QImol z6O;r)hDfO}LVX^S;g<5asbE{rmI%3N(Qsi=DO@l0;TmwnEkr?n8qL?p?GR(ZE;b%F zJoM+$z~LMlrf+}rcS}bkl>31AR_lqJ&SC zi0?P9;)k{F2aAk#eF+cZ{AcjSnkukd|4Wb3GjnQlH=!B*jX@`zga`A$nGZuopbq}h zteIAlR?`Gb(py?38#(!W_!~r_TIMW zuqRK}mg@`H;G;P6i-Th03yj8w=O3F@IZXIrOr*g-_)c_fQ+ePH*wt{_G)r32>S%cK zalyInYFG(2I8#I)Cm}Rnb72%d1}b%PY8JM&>4*n?u_)3paGoiUL4g~GWU-eUUBCV5h_p}_!P$F7%N1IN{0rj_R6h&FYC_1r z3%nAS%%o~s7^;3k6?|46{OvSZKhSCTM?^ANt~+$)-I%Az#mFtfHK${Wd~Vp2jU!z% zc9PN#Gp$~Rf7$MZd0C~H`jQ$3v$EowD>zq4Ve*iMAyc_>VP$?$1S)aftBkpEC#iF{ z@N(|V^=rPo@MsqBq9Yqwezgm98Y(=L2fqt2o2Sy!#iswDVjdbgp^5Q$EE%0JFbQt# z0TCf>c1jVn3~tHlq6oV<+vZBGRAqxZW8|z?ZFkkri`LQO5{tr!V6|lcvF9-0UoQaQ z#X1J86WG-VVp-lSF^Qxv<|rY?M`xx$!b> zM(%bNc}TIZF%`hnv`FcLu&_S)pgbDh+LUm=y6a2Ag6RI5-jq72_k9$a9%pq>W zuM`*+iwCJZi?~ZO6vv)y6nozr##SY)e8Y5rN?fL_H}ovkL4tOzCwXL+(Q+K7Z zjGML3k3VbWM_N+{HzWuU9PF|ILpB;Lz+lxE;&ileOKXlQ?CJ(~&MdAKj+RxD_vS?Y zq+>DB4E99eX=iYGB>+L;U=l6%)RC>2!LU_;r!v3xB6CgU%6yx!4v1%4sZab69dW{M zmZ@~3)BAO}SPAunFeOyi;v0h7N!rtbStUgn(@V8*4h~Lcmxg$`xL`osNfkd;KmhT% zIih!Pkzl=X&rsBeZ*24^4?I9S0IDNl^f_a@(y8o8BCIev8I+9+^151@ps$3pL&MX}Q5=Ig8dL zh1fqT<6QN?r7`FCX4Z5HumAsdJb~H77SN@l` zRYJhf>cPU7(+#05>!>3{I0lqv*=1F#pnCM4TM^(`V%_ifVqK4c2pF}MIUlL5gPHL9 z=BKxfIRsD49L4I2W7Z_74d{BxoRH3TvO$NDl~y)( zBF6eGYNoSs+XUe`<7OxS4}1R~ zzu9a2V~>5j5B6H;8Icj05s{Hs+|T{IPS&P27qtBm>?W%z+>756*t$aZCg8WL5?ctP z20e4d>kp?zlAfe7V@Z=&+vxMPAN4h9AeTKT5mR)i|JhUx>LM&swu1EbJB|MsMTN;- zP2p#`{2&=gMYY^vxKtdomT19pj0AB|VzF0g&m;589!{_YmC_Df#1EZa()NVO%Hg3% zdkRf?FeNcI&~%#dZaaRP4@!br&JEjy40{n0(#OO;_iQYY3-?l9+%dxil3G*~r#NlH zD8kbasql(TCEa}{ii3Hft)7!JSA1^~G)$RE`ns)2=Y?@T1k+ld^Pe>hoFg^GasH?! z2P5c#jnG8d)au^>wuh%4URqdR+80?Rk*dt&3-+%&Whn3DW?aIK5|@zob{V-YR}6)n zbplb7@|3dr&~nVe>nnI611&~*KDGo`+2+8x#%{j{J1Xy}XPz6^NO^2}O@^w{lsexi zgmdp;Dib7EYFbP7^4VVh2-t)EIX15};8#Fatastk8ULk$^~C&A(8EAepvFx8do?cK(O(%y$ zz8E$BgA6{Qx``WPdeb~V6KgKsY@ChAT_bxRBLwDT&%r>F+e;e4SdRbjO5Zh6LPy~xc%;BO-tvlP*W3=j!rUO?&Q`DZSt*ELhP;&W?A0}F``3Fi7 zDero56$8`Kj5=blx)8;2!Px-eyA-3b*A&K3?2f5ua#D6fz##g3O|8XrdYR3o$2 zSk``L*l)Wrw#Rl%(y#*@KL_vA%3DX!2bJD|^tuIHYwfON@kI)$g&4)Eifs7CTglcI zOoN(gB4$fG6=ugXQa#x)7z__i_)X=oV8fd`+Gj`5;+PjJkX)6O$rX=zArBhm0iqV$ zBProW3e;}i)vx=?b@_synHE+C@QQXnQFJvbo1N%3&UBn6=R7LbrCfc%u#uRTqU5Ua zb3P)=X^CK%+R?pSt(JH}2kmoGlVf~kRW=DDvSI$-~XA% z=P!rO+}s$mlAIc@jne=9^ZAx{)vfMot z9AL{<;H6iQpuwXzUpIz!=zw9SpO0Kd;paFBFiM+06An;b= zoI}6Z)Lsfc4VOJm&?CymAh=^LJ2HupX$jxuvM;7f;1Haf{W z8o?U~beTxaeIMRqOuhvOlLW*4<-lH;{lw$!mB)ELLLH(b0#(l97B!0pYa1crLNH^q zvZTG5>Y<%I1CR;Fa~Rg$>dDz?yne;pj6@ZevuECBBC`CHE|km7$sYk7)@Ii%=h)(K z-+kUXhV0z?WhdTt&H}*>HJ&p@2=9<(UZ3RkTMqzQz0EJz+b;Lgy!7$Vx8--={PKa! zda(>Pr$*Y**$J9k^7D-lLK_Ec)IjZ(1HYPWA#Hg->|@n#_T%lAwvMZqQFu!VymM$l zc%1K>3fiPXie%5U%eKw>ptLCUsn!hPL=NzZ%zmZ=P=1rlEu(TdFU-b%c7xga7o z;Ch*YTMy8il{io@Jtr?Lo{Fe8V`zb5hZ^44sFB8#7=~?p0wdW+H)!p)SU!y7W;T?ryST)(vfV}1s&&Ah= z1F_Lw(IWwJ_B|y^JC2;IPrv8c(Qww;s(F0gXktj3$)uTlH4{VfX#vcbZ!`z$(gFnUkG%O{i zVQ(H#KTck8`@D2i|XOYXIg%A>pyy*l=BhkL^mM2I^Z6NGL)p^$MJy^3RQ18WG@+ z&am%IGtu}*-=jgQwW0$!D&Yqp+15#fSV;i-%{A8h571s}{{v_pO{hFc{}cSQZ~!>( z)*(>hxzvB0H!H#q8ao4YMfd9l6aQ1EBRDoT);yP7;vdNY6NvxWZgw`(uUHPppO8}V zF&csYkZ`5A%&zH;TlU)hA|_3gs?hXlPx$}jb&XYe&Y6T>|Y$Btx=!~yV`atAL9 z(xcqT*c;GB{hvhmohFPDz%~qkM#v#t$5QmXc!fico&sPoz~pB;y>rDDW~Ls620+8B zM2s+FE}EgxYdPr29B@&EU%!sUeQoJPpFg#{C%7k|a-N}wh{zxT|xN5U9B2oR$CmoLh~NqM5<3Y4c&-p zrd5yYsw-dsLY^CL$L|ck@eY=11IQk_jD5xxWYH?sQW5;yppH6DIwV)EQy2#AU1+33 zUY@3&c0W3poYBMF(R)11{41VezF;4l_q?>|0%_b`+r^@dMhF?MF~9;k(X93r(A?<^ z34i?F!UO%>XHK)fTr=paQ62u!F}1sS2a2vJWLSP(Ou8ZM4jsMI@&~+?Khd}NoO)kK zpqo5b#qS$Pay$NNOJ<~JP|v$0+uiF-?MFG@+-jN`L*T-{!c^GEsYSs}+v zV*1x<_#eoFu2pBCGYI$*1n3i9^3GjAJw#L+<;RP<8cSujv(!m*eBihzx&`_*=v`?XN$Mbsf2ua zr(1JEJ@tozz;N01)Gy<2h(R%&hse8u9K>>d=P)~kb;;^glT(Z9DY=%P1SFni`d5MF z9a$?)Y2K51{aa)J?fPPjQrkTVXV81FW&)}w8}2KzTPMd%Q*xLG?-K#yDO@Vx*+Gb) zr}|**lQ#I}%Iy{`{4;##NEfuYv=FR=SO06%@C&yWyKdKQhC(cQsD>R?2fZSbI~s6) z%wIwA_9(8wd{qy6j z82nwLe^u1>x@=zV)j;RdXXqR1DgE+VSl%_ns_jl#>(~BSxHGw-%oU{Lw%`h@>|z8w zM_<_4WrzcN8Wy~D>IlVj^ret*|AOh$E*E9ko5Ws6f^YFJpKni@UKkKiNBKtsLdvLb z*UlaspELsG^bzH`fAGYtG;lBT?2WC@1~(>da-3Qt65g76MH9a)j)%52WXrF*0`$m$ z$#>Gg;22eA#I-t;8eEd?`)W9>4pAdw0hEj>n%{B7^Our&O<7I`> zNlSA5p^gzDHHoXnq|^IfVLEe&Q6aIx_64}%wyTca{2{OPlxG~9ZNzq0Ll8uNzgJ8c zYOgv2)s3je*TcHjxT7MFP=w}@{hVJ6yIM@9eBm?gJ|5Bm1?7}+(ysH$b!s~RetSe# zZ2N}6CF%UU;#sqY8=ZlNvIV@QbV=-#_Zgd?OSH-1=esAk^Ty;WV5`6|RiwJz7i35e z+}A5XcK?uqF!Bl;)NRwhr@e-EmexZFsj2;EoR~OrefaqOUP?;~R|?pN6R=cf|2(%o z>Lw7uIV>($J?RV-%Vm2<5kd=MiW&RazYz00x&5R&RD-Bog%WpNn_lu_*|Qv79o|Ck z@W^=SacwX$IW0g)Mrnaet==lHRFAe%$Ywgdr_0&Dsv44Az=F;*GL3U|MBsB_+c&*T zWiD`9aLvwtwAB2CR^bKJV0}#aq0RDh#eKTPm9^pV~@bAp)9HOd2~%KI&eLQ z3ss^cFpP7>12s2yy>Le(Iy(7WPYbd1SwHZ9bNjB>ExTFvE$9}L!v$QAyL8~;TI9KB zk=B9g`wZemW!bN);;i7~AFW)bBtB_LzuKC052(~$VPvKIVM4cF(7Sz_VbMa=$QLad z(Sm!Em~eWDrtwt=O8U15N}zGwfuF42;NBGz=-NLoxP1FZk$Qw$FRsv0YhMcSG*9)* zO9^zL7+Q|`ZgL&9Lk;7^^hDqDk)gBJ!#3j)YSLjXYv!J2?uA8m2pJ*nT&4zsj8afk zNQV6h&RViLcM6r9@I2sj9=&!V+LqeBaX;z8fW~+SbYzbTyf<6b*b|~gZTYANNsUbH zYC>-uoJ*Z6^$Y<~Zb0Q^@#_iE`EZL#Ex$r^b51c>IrH~CK0PPp8;jf_%#cyfM3TWt zscD5$bV2nf57S&nh&k#Hji7T6cN8JS+VK=?V*PH7`#a*-?C%j!E$lp==Ugp+X zuo@+SGCuB~=ZKc*17Sk4?rczYN=@|m7--3KUJjvmz8*H6vlc-&t)7WgqX^R9^?dMu z2-PT`T9&f6l^Izd90^q(weeV5nR;rrB5ZHXC5meXc}S@2yUxj}<=%sk3SALB>s|7W zH(&h3?#xxfk%Mzgczd^CwOdOmqpzmDq=WjrO-qtE+%6quyW#d$P4Rl~*Hyi`@1~#I zE#=Fq-s`-BYmOoD1Rzm<8=?ES;F(Q$6zO|I7bW3Yq}D9v6|o=ben!}@m~~30ydO0R zgN;+q@I682XKRM$S-)M=$(|D8^2_9hSj@$N+GTsLZH~*2k@Wn)n>C#F9qjs6`?Ofb zSW8N1G0g*O;UKK5lXt2I;Q4(bTVUQ+QgT`&;! zNBcb;#@> zD2Hqf|7aH!z3C1&os{x@U5^{x$c^t#&eg{c)hAq_r}wfMky|&2<2Lalcd43HGFraj z0si@}h=}~1`mbN+WM5sMwB63p5^-d6!owb>5IVP(y1@kAd75KqbZFz+e%dRGzdbK7 zjw|sBd|za}04ukefjc2W>jAj9lt((j5{lh2Llfx`_}~_aodfALjB8Vzqe+;&bb4xG$2B~7fSg0Qm)D_ud{<)B@P(SH{Sy+ zy4a}o-`+N?L9X_Ih+FD`eM(t}~;BiUGG!V>RwUu@&vsvIiqY--uM@T(B?xtu_&HH_6tOu0J}4--m(( z#Et2K^?1T<$!qYY&iSzej_*&N;yy)0NW~A&_o_7j=>)?7->g}-OJscm$nK}d311>if{5hsSaA(uP&|i)uv?*3X=oA-tEYutg)*C*f5KEhY zwt^k=X?FHs=J62N-lG`ff^c;5+BvdNgoRI2oo*bp4+_zAckI7&c}yznN_V*AIj6Wr z;njE`&ud>wZTUKYh+1uJE3Wux7agn>NLObjwt><7YU+Qo8u4mRvt|j=_kHY{c`!(0 z0%QQOg5EY~&!@aRh}nrCLr*Lq&mB3Q;+6wRxmYRqsa#`G#PyP9h8LnDb-o%JVw=>D z4{CYLsM9b+H6ux`ClVG&X7K5FXJ@{4_v|&fmumC@O@!BmoW@3KDEFcWVb0KdigsBU zvHlaD%r=iE3yRd*!A8sLYsUv#s}0`1-FhhhTVm={_64^vZ)qvnp@O-IqB%}R?yvlV zca$yyw|)C|y_JwVQuT=P>&crC1dZC!jVW)rj~-~*0-%DC;a4k^*YGoXU)N?%m&)~o zDUJPRfEu2(A_DD5@L-NlgbYupQc56#mi|T5R1Ztl$~{V?GWmzjIh?Q4pnJc_?5eMw zf;}=CUp;;_RN7lgP2Z5((_Y8D?z`pL4|Iw*V`>;zYhN(OxcA}^UPy=QX(!P9Srco% z;UT5kI^K0CD71V7ZT(HTTi7EU=ahPRm!^^KA@1VVrz9$X_RuDmZP+8NL{XwcKE8(! z=im4LxY)o_C>~LQua$+36mEAZB>JlA1^Skc!b}*{Z@|n_1*aA+;R}q`c626Bjm|4O z5Gk)|+Beri3q`+RUewSJPDUDcFXp3uLQ@D_C5T#@-Pv7X!7TJexOWqbr(^cl1p z6g(xvrW_i%du3eJCvTx_PuQFD8Z4pY%z=^FK(GN#(KU|Y=*^|CNL}q=@rY90AC5Qp zhOSlqkWp;Dls25w`^8v|UcV5i&;(6<<%!aepuyMfIMDo9p3$BT_WDIUu_gA8(DLho zs2FN?fx4$^`hs+n=cc@hV&7ugkL|NQ%?b#^8TxG%)bb?WPf=+F-6j-h-<6A^M@61x z25_CS-nq~a)&qQxoF_OMkWwnkGkg&XvqaE#O|`uqdOZD!LT&Ewjl5?F4Zo*imS0bU zUfOE0SEmK*(hQ@$h&(?O$da7nj!3fl>?a4eA1h8D=!Nylh4l&_M&!iztW{sc($`OP zDbdtALU%sKan=c6Yv|P#CKNBbY7TT$jd<#`*&9ldhjyEQG8o{ER;Z6VJrZ+YX$XBN~4CQfi`YHK~~bs*k| zh{8Hy)8iA~Wg$umbk}sC*JoD3IdDMHy1zU%-V{Fb4>HHgKb~MGZR{8V!U%X^9ixty z9l?VWD%VJt2k#WxcH)dzBS)-#Gnm3>234-$F zUD3%|=kRkE|Mdj*{q~_}n`=n%NSCsz$I<$yy17JHXEe`S;@F!FC6>X#w0kU{i7f-V zG`U*0^!@pdyZlry&<=t=yy_QM4h6SH&+E4UJO)VD0itnXwwUbi{sK!FdGeq) z4>x?C6krI2tZ}@QQ6SHEcN5%)UXfosKA<1xDd2u@uRNfr6yC^jV_x?c6}tCppf@@^ zcpobdFi$a#ov6&UO#_c?w_OV-4|XjqXirwT(3{jlXgM16qrKHs(4udzT|L{Gh|eg) zA1i#@Y#{F~7koEozX&gT>g)Xxp4U`&(J=owZO=fEq?YHb;eXgF3Rpi^X<{q7g?Ee6g8SKHdQ7jo&!O zg)-!Ft*>+=vOl&IJlD%m!_z5U64Ls4UdcW^c7mOVnobo}`3k_QWXM_5h}N(}Q}s&WT+R(i&<#AWWEk|H`OeAf6hu z7!BM^xCqbxjMm~A&^Bv*!w(-NMtFumn8EboBfxT%{nMk~4Wi~L#n7u?OP93)`SMg^ zrzabJXT^(K>5hSuR&+dxoIshdgFwA~*GYS1wtjoj*!Z*;9}M~|T{-fd_gA;8`qDKr z4S!PLku5r|*Tc7N=r<%9_ZgN<71s6pFd`~dIWrD zS(`qIL@5LAWF=4Ku6aFB@Ej52YJr~coVPnnw*n6)*%rJgYlGyY4VFJFfd@!Ng%ZWsn z=MI`ZA78cJ(|)y~E8JpWA9JGh1i$H^B8bW}+@K9o!dah4OSnJvaA0F5~(&mrs zd@b%z8OLhzFV#oN+LQL<{Zw~lD#mup{MZ!-pGXRQApkv2Dm^mr+6jNfZlTiql>CPa zJo}^Fgr$@fNu{j+0Y02USzM-;oZ2Sc!lkO&DDXZ>x+%_~`Yx*=W=VJo5CeLzF3f7Y}A^CZ?EUpc-J9*4UTwOn$W6MLz* z$xL`yAItCv%3=TEu!@*nhEBh|Q1q}mR9RPdzqsK#eB(*9AYgQ;?<-2zrr1DG>yz!( zLB-AQ_*M5e!)eWxJw{ER&rf~NMjLnwbxYOo0l7vdn7Xh`%_i;#vSRqu(jDZP+O_~{ zjjd}W1tvdd1T{|e53Ie2^s6-$yzIUidvv+%*!Q*omll!u&2v&_5OXFuxPTn?Kc+h& zwoU8aFzrRK;f^gy!lRNlb>ji#RzCkf99&FDFD9G9q{8gVsK;n$6+Q>E6IEe#`ZcQ4 zX6Y0XG`jGJOpvrd0>lLQeMoEgja!&@)HG51ClnK4PaIGflpm~ZBKIRcKJXv>jGwZ> zBuCm1Ms(7XCNr1p{a28@2Ki463YERYPm=#aSy}%PBwNKdpI7_~&11pWRROHdTPsY8 zk?KE4-T(n8WSY=uqW52+?PfthMADk1cD%-atUXl*d{nHqSYh??JC&7C!8@uGbC2rD zmG$zUrO&sr_MV9Fn3A2x zQvV12LBJ;rU0ZNi-o|I;VnalSQqpXxHTZafKa^*pTbV1@T0!V~I&seGmHat4ClYX!KQguSJR9nwRq5f>%ziE!*xXkN(Agh=^%l?f7$G0FIRURBFaMDn#+PuU?Q_$L4gFi&to zdZ`-=tJrllBWC6W_i3WD>etv;`Gdl7u~+J$M#@hO7+sC{$)+A=hrf1f4T4W&f5?Z-E}PuoxD;>#aP)Z+Q9@HQ4Ax zEEXZ+4?7%5LO=YvCiYa7;;#-cL<_c}Uu*0*>}2H({0`b4X8eztUAaiskl%X0V5M%n zc#QWya?d2MkSaV@DI^ppI7Y|2xE>FPd#96>lm&J)j&)vgpD@UFEc2B%^^}aTWq(-Z z#N__M*5{8Kk%NgtA*6(oM!6b#@~?l+ixB;};QcjBC0f*y@|>9w*4WU^H`0o&`i4w- zVlEUON;z)6ua@qwrq3%NVrGRVak!7>3r4g2%;Vn*Sd4b5Bp($DC!e$Kmp5Zpwe`l; z5Hw{X5mT=%>J0F@2^=Q-3X#bPj^_ahXQ3Kj|MG_7l%`P6X_2&Sji7eLREqgPme9sy zGhbu;pMbDUuadM+nRv0Q9xI|?FzK%tZ$pkXN4^=3v8B^L6S!%3w@CE-Ki#9_`x9BT zx9u#*fiwyE*Z*P^G=h6873ZR>F|p0-4IUnOLeYLL1_^%0ce3E>ygAWu%l-4}C6s=BBKk)c-Ady8n`mQU9|MH|feXcg;{VMn=79-O+_q zfdt?URh2tr5~8k3FSHM$X#IH_*1ks?zwl?=)w6mbnc12gL|+-XQBq)BRsC|NBJ8-~SXy?ZT8(M{^h5!LEca4|gE1HH zcleW-t{@_Z)=JHyk{en|n$zIeRd?=sh-!GfivF1Av7;jU{lBlq_u=yiQux75yKIHE zJ(os#2{MyILNbw3o^I`cJ~^2W1@iS4EKy&3jnz{pSURn&(B@UrPUZEO^-L4`8SgXn zsgS4s#aSb0lONt*$ON;IK*m@&LaIR(`yv_?Gb6RM!; z?kxMf3Zs$#y4M)bAzXDa+NIoaZX0Q7%jPzfBc9Aw!1K&Rj}tZNiWGYw#??cQnTcAT z=;qR+Q7F$J{#husxfDpZ=NWs7vHsX6-6R2R~``LqcQ`Vr>copmPk>3v2jt#N?G-CmD=ccVk1F;kA!8f`uTU1@DVaK z$h+6NyuVbs@v2Dxx)1`$K~dH^+}|m>1RR(Xlhr0dfU}x^78;E}{!!=Rvd5>nA}Je0 z$D=7{yy@m29Z6T0#SfJ+^F)+{r5M${d$nsX!%X;;;~Ja1y(9hk>R&%|kNmv-pb#~0 z*zOi~VIKwVCGb!VDwb?TONeFu9iH?B2;fjq9R{C(qjQ5}W0C*9__5%?V)~Sl9;n?D z#O%5Cf&c9u#DRYT0U*sZFoiho|K{I6BODB{f%xzvr29K3h4o4C0oxAg*kAhs|DDYf z1Ni{rsaE00Z{z>ps{a4vS-pQdSzE3kf;9IpW6|^>yvpCWr8&O|L1P*hXiz9n8m+C{Wpo$|F=r$Cj`eNcufH* zF92Dfz5$HVm(sq)6xQ59=s*0mUe`v@C%XuTKKj3X)u(`$CG?JvdQblvy0;Pn+Jb^H z1<0V__s`AkvLI>`x~Ite7XFs86Yz%er^w{a%^N!p;W|0< z0#fJsugrHfK-)VA2VVYmu9^u@>Cy{ut;gRo)!?-WE+WBqe>E~ zsD&j~<`^>|egu+m!IH9WbiZ=O?{1OU?a>a9&eud@)s;iGf!ID85Qz(Fgvq)(b_Q(&>;Q@0VTx1 z8Yb@a&$CggAN4M*%KAzZr05bS5eKBg)tQ`br#%btAKN7*pz%kycXz5<}k-ais$SRXI5QC?(J4jhPxS)qckV16i{wzp5wrNS->T#2Ds;r*FUa%UIc z=9%t_32xQ!lD}AZ5g@>T~9a=twzvC;Q=wIe2> zU|Z#HQtzsR&p<+oQ?i+(oFfZjcT|i`ZN)es-PuWsHQhbrP9>kvN{lNF<|B$rRRCVg z?ZkD@K#GfZ3>CByH69eBGI$y_Ge*Yn4BRUJeHV+4L-AW!beO2%8eX$Xb=hfAiM)~6 zs9`=$-Hg!jR6#e@->u5ViJ3J4hg8d?0y!i~6)C(_=kY-?bDk;Tf`ejySRA3dnBv#- zN(j)eiVmsu(&%$*|M|}IO!<_1{70VJB17*(^3l3(&QZNh(G>&vl zB1r42e6{~Pwu!NpD|yIj+Al+~j1@b~iM4E>wQDbHX3Wj#qPi$pMk2z(Y;ZA5U zo_e!f46(4d2TcMUkztRDM2|h(-E0e2m<$CS*(DYoUq)%UCB5CBWZIR8gTWrUiVpHN z)B=Fi!=j2CiJ6EUmYI^c`Dd)UnLVnhl8nfpVIrpw8=(#s0S}mtsy!iH;RId2c!`V; z{msH!9lK>Z2MgmUq}6SSgUsZnl|UF7Y(yjlog+`Oaf4TYncpg;pBfa;1+qb7#EN35 zD~*W)CnRtHdyGTbOi1VPy?Nn#aG|!bh@6PCF1M;R6QQvXEo2puB@<-95kb_r^(f82 zc-iKnw}|$){{H*YQg|Z=$zb9{6OnZT-`5NO^%f#VA zH=^SM^{uke0u;ec1Z_E^s~e3(FQRi=MC-+A*+a>@3=`Wp;qFwe3s&pMtJgz|`-Lkd zl`0|W1N}9HI+48*v|RVnu4U0)kJnj~l++y)FvDT}4V-Z1%f&}2hb;n*!&wN492k`6 z=>tW+u@uS6LEx~(UTDasrYR({!%~|ury@nU9RTG$2FC#s7Af&N-}@{+skwfbuqf(?Z>2F40Uh^S)$688u&0{xW9nde8UvKAM1l~`LE~fmaaGh9 zE2l46jz@3bPT{RYCr}T@6P6^6U?`P8>egI|2$sb=6{}?8v4V}UC&GU@JaFLd@&H!< zb$tBHMlvNaYyTmA`WdSJ_{40Rv0}CM=Ly*sV&PCP4;s0ac^7g`ODIHRUF}N-E+$Uu zj=jYCwh>M8t!7*GY-^6HY7ucuj^v7E zggC3XU6Zjt$>#^*Q-y|keX?X@6SNggjZRMLoSeLMgAzEuX_5p}rIp!e1)JM?N#(i7 z`KDEhD>xfyjw;18D>)-bVo{~4ErhhICqJWC4U1?`RGeJYAuGL(dqsKMmctg0rW8xHaQ)ECi?w5$RZWnR42GH7P&7O#pqPpN25dpl^*9Zf5A zI@groS~NXq7T@WW={VCQZ$OMY*K8qKHrq?I=-?-hTODx9k=NGMQv%>Uz+=v! zrBn9cCRvVJ;*>*iDs3qL1nG#CpkuJ`kx83{SoKDeqE=>M_K4HaPm7S-kI;sD7u_O)_hCRDOBM)d!`2RnqL-J^LbdMa zi3zbFPE3|cZ6Ig;BT=^gy3t@$!vizH{nLBqIb}FYriStA?+|s8>N@9VFw7oE6!KsS z)theom%+k;e~vqFNfW|U3+7U7niQ*@)Ig$YtSG3hTlY zb|Ruza3XYd0VgGqI2fB$n6E)*Ci9O_+1t5NzYj_pDzU&OA?J%@H0F=3O9XHt!H#~= zTZJ~S)cg@MbQPd=aD;-V#N?Ow$fYSiBE-nw+mCDaN=J;kFy zksd8EL|w2x%Lv>bqQnZ?nM>;Ba8IK~O=dpc;9{&>axa~r++!#APgnW-(kEoU`$BT{ zkyW2Rak02m70Ua|A$JrJqGcx)0|?>?J)Bt>q%6CLZ_=3?PgL+!2`!YAPsnqP;&K<4 zUI2YrMUol9Nn|ntqDV9^BLqmBlQrs~run^KeTl=A0g!aA8W52O4`*%w!E!ejSnxLo zqPdt<1tnRk4yC8w)MiK#P%ZFgTZ=&Yi=S8f(FKr>oeJVVg!hvWC+TqM#}%DlGHRLi zs`S4ETp62!Ct%osS8x`H++9(D6xoP!1ovN%%a??Kg{}BqZY9;d<8)~1#w*B)V9H)l zVA~$Kekw&l%LQOvs|Pp zLuzrNos7<^4qH%-ts^(gyY$*rG~IDWqf%BDP~4x!IthS7`Lj5k+f}CEI0lT9<3kdt z$`vbvr&6x-f_P$ad2;x6fx9}33twC$d<+@n(6u;+N^eCqH!2ri5Ny7z9T%ZOhW0+5 z8+S}e#*8N-gyX+XEtmM=KfDrgoll1l7`8uTd~^Ka3@Tg&9~k2D1BqM302?8k91~>MfwTC_wiC)<9y#jZ{#sVtQ;P!N;JutqCP-voU=|wnrn&~5M=H!wb7efrk!I@`Gs=zNL37ZKHs=g@qo0HvX19UC>c%8Pgv|JUsy}~-+%R8B zatqIaoSmRb895nIU`{BwM6X7c-#Tb=7Y^KCU)yATds}NpHhMyhJT9c)|C8y6yU`ag zpLV`H(x154h*?LZgQg9{@3|=5D!gGXLrHJCKs%5^OsgfBe|BH-$9A)Z$ zMUsc9#}7X>f7JdbrhRTP1XW3b6-}JdY(BVVGFMJJeiybsRt9D0tOWvgU9su~Mx)JC z<=abxs-5eA{lP|)ZgBx)U7L3iHw;!~mdv^CMVLd8ls=52k0#*Kw}9c`XfY&y2n z{qim#k720|MM?xy%Q+DIOxo=5c24CqO@mrfnFt!o`CmLWsBArO&VmhbxOtVoI5ce= zatN^w4id$zVc}ZNnAKtSU;tkwP(gk8fsLt&e0o#iRPLFwP5neHa_^6*g=WYo(HVT> z8AbRYW0CUFf?9HpqTXJ9NDRf%W^R~>6O8FWwMH#XIi$gU_Ja{k)&c>s0&vJSF~BFk z(zr-x15778P}O07T%Lu102b7n9`I6V=cllnvN&-zct3vSYKl??omGub7CTg~G=gX~ z?`zr4txmQpZO&GScv>AFjf`J==^T1gl9^JVwwUioG6m6R2uJzBgc6xzN<#-ub7{7U z?JF{`yGUK-O!gSV*Kyvs-AO%i*% z7M&tjjtfP5QeIK19HF{-ap1FR>pK)am%MK-YwH=FYTgXjc|(`^F<8OZsW|rySV_#u zW?%ArRI+(-L0yvxoAva1BH{q~YF3d5Qi<_Ec67ZAHcPB3@b)Amw{0Aro2fxLIyK2l z6FIreX%N17y?W2I;$rHlPY!Dmi%CJ(wqZ(mwoXRtcy-;N9#VOAsnm(JvaJ)4@0#Bi z9C}xLb-}9a6~#v0Cit}t;agCoLVHkuomia%CXWz}Xpv2G@e@|IBSGJeJu>KKzEer2A+}Lgjby#JiPdthg>@1VL1$98WW4Or(|Yz>$9cV25>FD!=!-kmB%gH?>n#^=1$ahNl@M3o{o~#Of9={ctvDF2ME@dH8k}2!Q z`p%pUl|tlMFk>Di zBIob&sj$YYoZ`dS;ckHh;fMI1r)y?SEKEKOlxEQ!gryMfPM5;2#P}|NEi2igRA};Z zMB0+yGR&O_9lr^U%;}yM{cb&CEM87ncq243-?ZXzjd->@Cen3K<^HU$(c zOo5Q-yvZ<8J0Y~a#0j{oN^6T(7H*xB|BRCCvf!U@C=f9UCHdcj8cBtU%L;G=7pH{7 z%v0T2q-qHSW5UtOvTlFlA8wcBbrrVRPvaBwWfL-(tHbL=s{{Sf6j+LHPxLt(wxjW4 zP5AbW;*W%|fO1I|T1z)vAOCB-^>SkggjOk$r@jQR4j0R)n&%&g43!n;eAoi1jb&j= zR>OSrAb0FJHc`%nH7TWMcmOD%{P*XtfDZDAv+taTQ~WS|NSbtY?xY|tOE@}R5l3dT zXjVoSufrepbcQV}zJ6ZHru?nhdQCy5#ARa0C)b4=9SaT>Ux1wxGUI1)5ru5PA@m<8 zNO)N`;y2=td=?^fi6ISjK^2n?YxDAe;vsb{#~W$mmT?OnAwkp=Bx^*dQ^ukISdDl; zm{XvZcAhlk?xHxgXHn$X0Thy`qWL&{Fq+F*1gz!){U(7VQKpogu+x63SCp1;Ob&-z z)ryQ~pO65jCWsWu_JgFq~h3g)rskoW`mwqwQ1o9Lti{h)1&Q z$F+e@$X1yZr_iF38r2@WL)f3JW1NHn)We7(?KxCU>5mMWUE$}L2d3_bL-3umJc zVIpDua3Fysxw+Iss}Qro%PI@?9$v3O!4;38C&7>=1UahX9vh_?n;TYh>A94Uaw}2$ zB>nBA=VR-^*2X#eh#e?H)7rlc!a(dK=wnm1l8ZUael#D>y~3(gSp>Dd;~-^Cb8FJo z3G*_k`@c)u^Mat;>!c7UZgu@q-RQa!a+CZ%heTM4s59VcCQ#}$L$F+%g{rC=?%Q%VX00%Y>H zNJr~GS^WtIUA!Or6VN5r1|Q}ktfy*F$c7zI0Rxs60@|W=tw_}rY;<;o4#+{(nEo~} z0Hx-WcxQR2f^IC0m(QV}C&4atci}G7J6gNcBxkn9S|!C>2kmH}jhF1d;N#yf1GKQ*B}2btiob%pk+9 zEioV4&ZkPd01@vekaVwqZ`58P&crSb>9r2gOqj>YzUYx96t{l>YSU0NmYjA36{ zcI-GKfHtBF1F^}rdctFk<)`&V$??BOjF7%O>(K|rf{``lpTd8ixH}bz%VQl#$nt z{0l1O0VtDaeozDNFJoi_&`%d6WHdmU&VO+z{X76V!m-#dj{MigUKs#V+yEXL`d^d% zAfQQ$8B(ZR1}5>kXnsX-kBlQa4=oJ zb;g%o4-C4WNo+OweT;VRso|IYp{JuQ?c{4pX&zCv%=cZxEdp4J~TgyFrvk`ke zG!uih(=_6SV>&Na;L=mZwdkA5#7p+~wdAdZu2AQ#_XE#RiqZjhOW4{0daQ;*K-&A<|9YQY*qgp-41iX^7?LT~n9Ft7#T7P$+iWiD< z`{7N3H=DR%4+US~?bXtZv^Smfh16QL_c-^^VO8ZM5tSVyN1G)%8E;wC+s3{@==pxI zOl9*#M4-{-SNzVqNhkUEyuZ5>l9z`cBqJlWD}XoSy65d2eldCmufZB3Zr&xv!snh7_2WQO=ye#@zQGl$s9f6P2l3QxcH_nATNF77HLJlLZ+y}T-@t>hJEe`yi zSI^LLJ^F*Zt4~ij0c50GfyKo?t!&MNkjCV9L62^p2KIByCZxX@+{l;F4vR$#*|bF+ zPJEnB^RH)gSZ!A*OkxJ}8Jg+bGuohhc$>Xty0mi0HU|tHW(cdYP=DnfdJyMW^^I3< zkT2IF3*d6SMK6MbSu>{DytcXrl|2 zZf*U-$yOl6pI@=2sS&YZ&GgUzS95Ru6vy^PdnbfIfP~fQTUz1L?w+vCxRyl=s$9Fd2@z&b3HHh`fV?GZOXi;4P3zKXi_| z{b%M{CxctnQhL?63Ek3dA`}267I7UGdn=y%2J`QGF6h7B{q5LdgWw%}+}!aE%jfU& zP_!hMh4<6zXGT_*P5wamO1se|kt$2rHOC=46@|9|+{&?0!fSinC4H)B z&`w9j4VLCl3pc)9{hEKfBh9I1GIlu>Ppj5wO~lR3%{VvM_B30nT5Y+=eE0YWS$~19 zpPhAoh7=cObXDnhyA}b6-fSLo{ZmQWN8u5U^Tu_#BUN9|%`zJ%6hfY5=f|B7-@kqx%%_tVXny$F`TO#Q>}_axDL`g(>#ugv_##6+e8Dfn<0A6AtwE^A9QP)u z*@1!UxbCE=X`S4ec;akUQ!5sL3s zYwJKCe54(e_#O~cNh!`@_RaIskJ=IGmRB{Xd^dVP>sHZ^(+s?I%s~FUk0)|Gh|glm*oNJRw_8ZFr2NSBU;>wSSFo=J>VQzW&s zUzhu6w6xa~r)#q+Tu*6BShQ8(tWmL>n4uUpOcs=MJtxrGdT^l>7o3a?Ay=BZ(aRL z(NEiDXp6@iHeTCZ;P0zh{17XbW7LGe7?to1|N%_a}7a*S2q$|pEJ!wXg`3-kQE=t= zxRX})GxTRJMbq>`|fK|9@*TB5?=MLKt`QW2U#(T7%-KcQ}%&qfGbd=GOE zGJV^sYhB_k9-ZwG%mrw)3PveO4`h97yaLvs^p_8xWS~>W z{;KImrY#*Yx23_1X`@JHpGwiaCiNz>r?2kVv2PrMyn|U=NN8ty{!FwjPdLb}sP~bd zHEZ3i(1acL)o+==Iq5b`{*Z8i5_eRvZ(456f&_q3^b_ghm&<5dGLef~DO%7MOC#un&+@r3uEY1ZP)I`FP z814xw!7H5?aD+7*0r+6}VbY5vTc%fh`^oZ~vZbQsC4MN{q=Xr{3H^KB-aemawTHcT zLD}M}!j?useLQ#6Zw zSy*Tnf8)MO-xHJy)PtAi5a>|hHkBN8<4jc#k_5OjnGZthT8yZuD5HGawL(!A86aJV zg^UIl?{F*Mr@Gtt&d-xocI^mDL*7;8ljC_kKP!kGEY?oc(}kmls1Pc2K}=8j^5IV%Kz9 zKbb2=q##AFF|@Au9jW~c&oSZH3maMpwww0Znfa!+{7rDHO90fS8&qr49!NV=aDK$j zy#RFbTib-BH#W9PuXSL^c)@Y=uSQxf*hCnpeD(AMViX9yAg;CyXyCwD?-CVbJK=Rdtb2CePcqE)t9s+u zAE0B~Qe_z>rB^Mr!Hx07L}+PeN}Op6``v-v5Ti?y?E>@^vOPE5#N|#{v0>prc>f`B z?IoO0{b7wKurGfoNHw{Qn^LT|i$TY^l?a6OM3~#PmnBUkG3}dw*5L(LF?Z6x$nOr% z@I2h?kmFqm7n-451t2 z!x5X~D!6HMGPv^TJ;ReAKL&s%KX{O0XZaVuK1c?ACtWHeW!3k=2$)~02*H)H>UdH? zkjdneB;Unnn|~2v{Jo|_Nt3n6iWrNWQ{-DsQm_qG_yK7?|Deqh?r8+yRB}jE|1D+$ zlyl#kZraR)x@&2(a(_g9$Jd6>$I^#cYmjy)yCrgfF`F#zNI%0@tNpnBSHOANy8jsm z{G;Oy#Jz9xDB`5B2oycs-cfc7=b(flP5U0qsKH4!)Ry4YQn0~?V`_n+kN>;Huy;a{ zqDI91YMR6`wTj#?1MIJA*gE^XY@8bsQ%JM^^il+M9A`fiXkspmc(X-jPEO*XDDuOD3nyH z5`2TNprBA?v(9(F)L=#62f-hCAIRE&}e>(d9Yc6Bt$>)U}5o)7*!*-Y;f{s5IQ~dh0SACMjqZm$Y{40aA2Zb zI5FV3Yq0W@!9nN8@-myory~;^X#CC6A_ccVIvcBUR=%ukw2sEccf;&ioL(85Fpm(? z3k|M^;~ey1TjukSgCMr~Cnp4%w({ef*M7e8t%_#SV-h)hw%=G7r_#rHXnt`%QLo;4J{798hf*=W*w6trNJz{d<)F2CM`F+(fa6s7w!jS+C!GrW&9DF=b^757A-C@B2{Y+wR2&+7o| zjaFRGGHYvmqe)??Ay_kV%N?%Zq^=zZlj~yc?>rmbk<}Y>$2O{!5-Wu~ADpVmwH^EV zIi65lc}2u6!h`b1NTnI8pS{gf2CO7JHyhR;ohj%?B(3BuT&y(iX5^`Mqcmqsxj>qH#USE)nFdpVmE}Cuj91hUpN~P1q8BwO7vp#!H57 z7{`5GE z1&i?;)Fcv@D<}Y-w-$sgU9R39(8(UJ2F?P(Ra;J8EA#=I^mQ@XV<_@b=1Wq6P9qvl zfM4xrUvG$>-&jcl&ly;#a$<32z>ZCC-=dIoVb=PC3K*ZCzHE#l9J3@C?gq7@2^wSW zkTH?)aLW>@D>gq8x4e6)^a{QL-E2)mNS&RBoMXg7clk>D3FuX;A?qaaUhK+j;7pR* zU!y*_T%miJy`K|;uKM|3(6{anU}oID12-Y3?VB7zAk~=_1*7E?>)o0VZ8b2gyU0Zz z7xR#|dwfqyj!YwG@|Ld0kL|PG3YKotOBITC9koTBS{w0eq5CSc)pYFIj!Uk|luRrSASl8eCRv}3%g=(0dBWvWO9J<|3+A&C!#_AS&` zYF82aZKDHu!{Er&<<+O!)(Go&v-Vc`D(4FZ2V6zO5VI0!t4Rb(xg zpG=l#NvP#fCL!{tX@J!nnX-NKhysBMQ>MX&h_+et3*TLVr-NRJ zEVCC5(_Ue4m%8YUuOAzTUzCOes?LKSeaasj!b_cVvwIt88``+fC_IABz>Tlcor6xU zbIF8Y?Kk@6-b-!NN^uSOlPuBqFnCDijLC_eb%PNc$BKc6DISsHpopV?+tp#fwg@9d zCdC3)pnJpUJb?H7mwjU#k(=S;M`x(uy1F1w^5yI@r!11pT9a{65fT57#;KzY7%lmm z-GStEbW(htvrx2CW^WrF@6v76w$q5Na*&1eL0o7u{!Ft17dKWQ-^dU9Pov3eubhGh zlnQ|It@bFf(z4S`U-{t}bednjOJ(BcwRwA`5aQ7GTI1N;6qDQDYH9;Y z9aBgJ>yjl$QcO$EK!qPumk(rY=9!qmVYSB7rrs+CTVnc7KYi|#2{k?@UedJb-gEhP ztt$v^0w&V#E!6wkJ$&n1mUCa<4+|E(I&8~2zxeLiZPRMr^_loZK7mhwdwjuWm<5A< z&5u-FomLlOB0VL08P8uqJ59O~yYUBs0^G~-$}_n@spIFy+V5k-5k6^!89&%d(Cu!o z7vq&)6;(`H_W^EyGabG|r52Q#rcdoMZ3-Al?)0&M9b1&4ON;xmmt#4K8#Kw|Q9KWQ zH#FnQ#%#KN6`y9BaYraT2l7fvkXe2?Xtr}F{Zwbi-1qM++`MFDKHJs#d38NugzqT( zXD1DpYx(f-*GM+{m3p0*RED~2XnUVWxn-${=&I0ol0&IsX}P5Y9{HfUhiR)UhyjM( z?6)iusPgfwlq#OeSMuX2032Bd_)Lg`G&9^nGolwGQ-BTK={LxjY4t8}gLoVlyfaU3 z0p|JkyA?2w#`U>r#TgZp_#K*kr6K!$UmjSFgq>-XUWeS=jG7A;gRr|&HQCTMLn+U1 zIKqE8IW< z_E5LwcLy$ZUamS?Wh*afqRYPV2#0yR7$;71oSSKHlZQxZi%LYtx1w=0t~_dbKEBqHCD8)a~5QKQI`5Fh^Yycl}^# zg3ZDB_WYEl*Q>k5;UK2p%TV(PRJNg)qOtJ`^mwQHcAaz0pt07O~ z{S5}%YyQw|N6FJoLyG2&HqbWS^JCC!;T7narmFt+aAz2o-CXap#iu(ivsA9O-v3!K zcPALQytkeX+Lrmc)flpgwhtAUXhaPal2Bax*UyN4KHaPDy87e)A;FQoE9P+0elGu{ zE$8>y$+>{UjqJ8c>sj^~z*F+QWq^h0Ri7n~^B()dE&lV8a2?bRcb|M-9<=k2?<~c+mf>L<}1?ZJ-{I$SP6pmHd zH!&AwNYtT`@xT7m8I*!iTxKck;#yN~DeFn-^d*OdLs@S)0r_ElXgzI4sTwKuCqX+G zLT&!P7ua@V;MRF59{_$>-4N@wI)?o54#CU9_9#6e^7-?}Ca%}Rf+P`U2EyL9FOm}Y z97ATr6AR>#7CtmcTWzqb=8$&x{e|}R&606zpPuJ2{t8Dnj+eVNhX&!&VQAnSrQSmj zrZc4DLcUa~bNjwp&2F7yV+iSZ3D!5w{3XsmQtzI(kz0KKC?ZgFs+=>f77sR`CRE0C zDJZcmCL2_U$&0pKJG+abA417CulodrOC3$bY($%jXDgV?$PY?#utp-J1mbxt1FEUJ zX;`@PKm3eZ42y{q#!^n*XNeiw`zwzh%$^#Bju%cQ9k~!0Asiu(qS%iF2+Kb_I4JyfWB8zme=2yMReoG%eS=i zWS{s_RYNxLT!m(!aqaWlN>GB9u_SHd(XJ$-i(9 z@wfAnC=+EBju=x0Hdzt+d!1_Xb2 z(j-(c#MqCP&$_7LX9hGMG*i>kiq4tC;Cnd2sVCz|&{G?e zIW&T%7r!*>m!%NnK}CY=MRb(%4_9O~Kvd*i?~y$H6Uc&q=EhQBBn5E3Fb55qK#~0F za~iPA#C?)rX8pCaX$SM$TEHmcy=0X2JItIk;F`9Myqp~{C#$-*EGiJ1 zRN+p2UYdngDd7M#BkCRGR<&pnEx(FK=8o?q$ZE_O5Y;EZ$0Tbxr7#z11@=xUXwnT6 z=BE4XP_SetMNk*#?$qTsG|WA41BX-#6z?5E(bP4g^n%63YlNe|k)CU*$Mm9!T@zgu zbrlISAIQ+~N#9^cee}(5QkfR6`8_zdttHuFsw!L&$6&6(l~zz=lzUf7PsOWLkPAto zlo=|UjXMMWYi2vgby7;M0^R63o}rm^neOM_JO1ln)8e9vTCLv`c8dzGRcIR6#(^VW5W85r_m_%w67Dk`Ynz2caa+3 zaY+HK3+@f&7JY(;{}*v0qao(EtTFlF0R_Wnx#;#klAZ;KK1N5^BsEJ6toYWsV~9?f zA8Imrlu%nFke>0TCJdps($TGdG*whh_V<&`Z;y_V5-C=zA2p><(6U97pH7HUE~R!L z4xDR4rZB?FVxtOB$+Pi`=({flv(i?i7nG8%U@ZoTH3VsC*E;*JEh82zer-{6YZX~mNHww zDLb0zIAzD(&|{P5*uJO{M`$Y{O64%K&$xtOAg|JlPKAD=Q;!$m`#o#^UZAOV;h5PH ztJ)@6Lta}sQg4YZ*rT?nG_RPpR<2oLd7AG!2!-5BLnCCah0${6XcC{m2Ov4@9fDTo zNtJ}GLLRk7kTUT1u)Hs~Aj0V#D_BEICtmV$Oil1XK`^i0t%w$0=MIzC4ApVr}m9!LoNaX?=4k|uZiAupSV<9M%|~KR#d&I zsmzHq5Vdvw{5@nmr#;wx@=aPD;%=TVIuXkDbX$#l1p;R1fTjHu_p|%&TIrzmxWES$ zmXUI*ZL5&N0^k=degyqs$%(jT1j^a2#bkY&@t%Yy_q+SzF`6&pV^*@dVBc+}FI44e zL+{Cd?&mdcm^TLvhm~}}V&1#hqpOo;f9qpV?N$48Sm{4u>r3WM&x;9X8)(wde6&lV zA}qG1*)m#YS6tQ@JPmEI66AwJ15`B9$n^~jeM?7$-xr&1B~kfR7~8$>Q}_F#=kGKY zDjubrRD~F+2;7{}+(1YUOK0%Ej0r;EpK+qlDu*T972;+bq*tEzI*kln9nrqT;g|zJ zMBbh3dgnO8x+LL-QF71*WPFK9zkyk|T+QkI&f<1fA&;MQWQ=bBb!FJpI5B@njE62w zZ%XMljZJ#w{2zSQvuM_04$GDByaKkMdBNs@56Ey9U(pDLYre*{AxHu&6`6$$qO}`R zR&bkZ&M1s1qcC;raW%Im-&hVT(Tk;GNi<6S6{lVZc}kKV2&TzhlVm#3@>dSMoMlGH z15iXis>JBnUpQg3*gVJ&EPv5D!JuTVKw%Sa6(UKM@t~u`6PNvM!BcPLoq{QdvW(QOOY&R2)VkN6GggotJ*nZN&y!`E{a%2q}14sADw2e=iid* zE11>17>3?Ldv^h0`H&${h;{8(ba!EHvDFd+Z`7drF9)N3Lr85B}qQB$Jh! z`;J`nZo*=tzxUvv@?rbKVPNa6e^P5<9{j2HeAjjU3K_Oj$RutP#bMz}&y&*%RD DV4kVw literal 0 HcmV?d00001 diff --git a/doc/ci/raketasks/README.md b/doc/ci/raketasks/README.md new file mode 100644 index 00000000000..872be4dc966 --- /dev/null +++ b/doc/ci/raketasks/README.md @@ -0,0 +1,3 @@ +# Rake Tasks + ++ [Backup/Restore](backup_restore.md) \ No newline at end of file diff --git a/doc/ci/raketasks/backup_restore.md b/doc/ci/raketasks/backup_restore.md new file mode 100644 index 00000000000..eed12c46247 --- /dev/null +++ b/doc/ci/raketasks/backup_restore.md @@ -0,0 +1,237 @@ +# Backup restore + +## Create a backup of the GitLab CI + +A backup creates an archive file that contains the database and builds files. +This archive will be saved in backup_path (see `config/application.yml`). +The filename will be `[TIMESTAMP]_gitlab_ci_backup.tar.gz`. This timestamp can be used to restore an specific backup. +You can only restore a backup to exactly the same version of GitLab CI that you created it on, for example 7.10.1. + +*If you are interested in the GitLab backup please follow to the [GitLab backup documentation](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/raketasks/backup_restore.md)* + +``` +# use this command if you've installed GitLab CI with the Omnibus package +sudo gitlab-ci-rake backup:create + +# if you've installed GitLab from source +sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production +``` + + +Example output: + +``` +Dumping database ... +Dumping PostgreSQL database gitlab_ci_development ... [DONE] +done +Dumping builds ... +done +Creating backup archive: 1430930060_gitlab_ci_backup.tar.gz ... done +Uploading backup archive to remote storage ... skipped +Deleting tmp directories ... done +done +Deleting old backups ... skipping +``` + +## Upload backups to remote (cloud) storage + +You can let the backup script upload the '.tar.gz' file it creates. +It uses the [Fog library](http://fog.io/) to perform the upload. +In the example below we use Amazon S3 for storage. +But Fog also lets you use [other storage providers](http://fog.io/storage/). + +For omnibus packages: + +```ruby +gitlab_ci['backup_upload_connection'] = { + 'provider' => 'AWS', + 'region' => 'eu-west-1', + 'aws_access_key_id' => 'AKIAKIAKI', + 'aws_secret_access_key' => 'secret123' +} +gitlab_ci['backup_upload_remote_directory'] = 'my.s3.bucket' +gitlab_ci['backup_multipart_chunk_size'] = 104857600 +``` + +For installations from source: + +```yaml + backup: + # snip + upload: + # Fog storage connection settings, see http://fog.io/storage/ . + connection: + provider: AWS + region: eu-west-1 + aws_access_key_id: AKIAKIAKI + aws_secret_access_key: 'secret123' + # The remote 'directory' to store your backups. For S3, this would be the bucket name. + remote_directory: 'my.s3.bucket' + multipart_chunk_size: 104857600 +``` + +If you are uploading your backups to S3 you will probably want to create a new +IAM user with restricted access rights. To give the upload user access only for +uploading backups create the following IAM profile, replacing `my.s3.bucket` +with the name of your bucket: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Stmt1412062044000", + "Effect": "Allow", + "Action": [ + "s3:AbortMultipartUpload", + "s3:GetBucketAcl", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:GetObjectAcl", + "s3:ListBucketMultipartUploads", + "s3:PutObject", + "s3:PutObjectAcl" + ], + "Resource": [ + "arn:aws:s3:::my.s3.bucket/*" + ] + }, + { + "Sid": "Stmt1412062097000", + "Effect": "Allow", + "Action": [ + "s3:GetBucketLocation", + "s3:ListAllMyBuckets" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "Stmt1412062128000", + "Effect": "Allow", + "Action": [ + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::my.s3.bucket" + ] + } + ] +} +``` + +## Storing configuration files + +Please be informed that a backup does not store your configuration and secret files. +If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). +If you have a cookbook installation there should be a copy of your configuration in Chef. +If you have an installation from source: +1. please backup `config/secrets.yml` file that contains key to encrypt variables in database, +but don't store it in the same place as your database backups. +Otherwise your secrets are exposed in case one of your backups is compromised. +1. please consider backing up your `application.yml` file, +1. any SSL keys and certificates, +1. and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + +## Restore a previously created backup + +You can only restore a backup to exactly the same version of GitLab CI that you created it on, for example 7.10.1. + +### Installation from source + +``` +sudo -u gitlab_ci -H bundle exec rake backup:restore RAILS_ENV=production +``` + +Options + +``` +BACKUP=timestamp_of_backup (required if more than one backup exists) +``` + +### Omnibus package installation + +We will assume that you have installed GitLab CI from an omnibus package and run +`sudo gitlab-ctl reconfigure` at least once. + +First make sure your backup tar file is in `/var/opt/gitlab/backups`. + +```shell +sudo cp 1393513186_gitlab_ci_backup.tar.gz /var/opt/gitlab/backups/ +``` + +Next, restore the backup by running the restore command. You need to specify the +timestamp of the backup you are restoring. + +```shell +# Stop processes that are connected to the database +sudo gitlab-ctl stop ci-unicorn +sudo gitlab-ctl stop ci-sidekiq + +# This command will overwrite the contents of your GitLab CI database! +sudo gitlab-ci-rake backup:restore BACKUP=1393513186 + +# Start GitLab +sudo gitlab-ctl start +``` + +If there is a GitLab version mismatch between your backup tar file and the installed +version of GitLab, the restore command will abort with an error. Install a package for +the [required version](https://www.gitlab.com/downloads/archives/) and try again. + + + +## Configure cron to make daily backups + +### For installation from source: +``` +cd /home/git/gitlab +sudo -u gitlab_ci -H editor config/application.yml # Enable keep_time in the backup section to automatically delete old backups +sudo -u gitlab_ci crontab -e # Edit the crontab for the git user +``` + +Add the following lines at the bottom: + +``` +# Create a backup of the GitLab CI every day at 4am +0 4 * * * cd /home/gitlab_ci/gitlab_ci && PATH=/usr/local/bin:/usr/bin:/bin bundle exec rake backup:create RAILS_ENV=production CRON=1 +``` + +The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. +This is recommended to reduce cron spam. + +### Omnibus package installation + +To schedule a cron job that backs up your GitLab CI, use the root user: + +``` +sudo su - +crontab -e +``` + +There, add the following line to schedule the backup for everyday at 2 AM: + +``` +0 2 * * * /opt/gitlab/bin/gitlab-ci-rake backup:create CRON=1 +``` + +You may also want to set a limited lifetime for backups to prevent regular +backups using all your disk space. To do this add the following lines to +`/etc/gitlab/gitlab.rb` and reconfigure: + +``` +# limit backup lifetime to 7 days - 604800 seconds +gitlab_ci['backup_keep_time'] = 604800 +``` + +NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration). + +## Known issues + +If you’ve been using GitLab CI since 7.11 or before using MySQL and the official installation guide, you will probably get the following error while making a backup: `Dumping MySQL database gitlab_ci_production ... mysqldump: Got error: 1044: Access denied for user 'gitlab_ci'@'localhost' to database 'gitlab_ci_production' when using LOCK TABLES` .This can be resolved by adding a LOCK TABLES permission to the gitlab_ci MySQL user. Add this permission with: +``` +$ mysql -u root -p +mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlab_ci_production`.* TO 'gitlab_ci'@'localhost'; +``` + diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md new file mode 100644 index 00000000000..68dcfe23ffb --- /dev/null +++ b/doc/ci/runners/README.md @@ -0,0 +1,145 @@ +# Runners + +In GitLab CI, Runners run your [yaml](../yaml/README.md). +A runner is an isolated (virtual) machine that picks up builds +through the coordinator API of GitLab CI. + +A runner can be specific to a certain project or serve any project +in GitLab CI. A runner that serves all projects is called a shared runner. + +## Shared vs. Specific Runners + +A runner that is specific only runs for the specified project. A shared runner +can run jobs for every project that has enabled the option +`Allow shared runners`. + +**Shared runners** are useful for jobs that have similar requirements, +between multiple projects. Rather than having multiple runners idling for +many projects, you can have a single or a small number of runners that handle +multiple projects. This makes it easier to maintain and update runners. + +**Specific runners** are useful for jobs that have special requirements or for +projects with a very demand. If a job has certain requirements, you can set +up the specific runner with this in mind, while not having to do this for all +runners. For example, if you want to deploy a certain project, you can setup +a specific runner to have the right credentials for this. + +Projects with high demand of CI activity can also benefit from using specific runners. +By having dedicated runners you are guaranteed that the runner is not being held +up by another project's jobs. + +You can set up a specific runner to be used by multiple projects. The difference +with a shared runner is that you have to enable each project explicitly for +the runner to be able to run its jobs. + +Specific runners do not get shared with forked projects automatically. +A fork does copy the CI settings (jobs, allow shared, etc) of the cloned repository. + +# Creating and Registering a Runner + +There are several ways to create a runner. Only after creation, upon +registration its status as Shared or Specific is determined. + +[See the documentation for](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation) +the different methods of installing a Runner instance. + +After installing the runner, you can either register it as `Shared` or as `Specific`. +You can only register a Shared Runner if you have admin access to the GitLab instance. + +## Registering a Shared Runner + +You can only register a shared runner if you are an admin on the linked +GitLab instance. + +Grab the shared-runner token on the `admin/runners` page of your GitLab CI +instance. + +![shared token](shared_runner.png) + +Now simply register the runner as any runner: + +``` +sudo gitlab-runner register +``` + +Note that you will have to enable `Allows shared runners` for each project +that you want to make use of a shared runner. This is by default `off`. + +## Registering a Specific Runner + +Registering a specific can be done in two ways: + +1. Creating a runner with the project registration token +1. Converting a shared runner into a specific runner (one-way, admin only) + +There are several ways to create a runner instance. The steps below only +concern registering the runner on GitLab CI. + +### Registering a Specific Runner with a Project Registration token + +To create a specific runner without having admin rights to the GitLab instance, +visit the project you want to make the runner work for in GitLab CI. + +Click on the runner tab and use the registration token you find there to +setup a specific runner for this project. + +![project runners in GitLab CI](project_specific.png) + +To register the runner, run the command below and follow instructions: + +``` +sudo gitlab-runner register +``` + +### Making an existing Shared Runner Specific + +If you are an admin on your GitLab instance, +you can make any shared runner a specific runner, _but you can not +make a specific runner a shared runner_. + +To make a shared runner specific, go to the runner page (`/admin/runners`) +and find your runner. Add any projects on the left to make this runner +run exclusively for these projects, therefore making it a specific runner. + +![making a shared runner specific](shared_to_specific_admin.png) + +## Using Shared Runners Effectively + +If you are planning to use shared runners, there are several things you +should keep in mind. + +### Use Tags + +You must setup a runner to be able to run all the different types of jobs +that it may encounter on the projects it's shared over. This would be +problematic for large amounts of projects, if it wasn't for tags. + +By tagging a Runner for the types of jobs it can handle, you can make sure +shared runners will only run the jobs they are equipped to run. + +For instance, at GitLab we have runners tagged with "rails" if they contain +the appropriate dependencies to run Rails test suites. + +### Be Careful with Sensitive Information + +If you can run a build on a runner, you can get access to any code it runs +and get the token of the runner. With shared runners, this means that anyone +that runs jobs on the runner, can access anyone else's code that runs on the runner. + +In addition, because you can get access to the runner token, it is possible +to create a clone of a runner and submit false builds, for example. + +The above is easily avoided by restricting the usage of shared runners +on large public GitLab instances and controlling access to your GitLab instance. + +### Forks + +Whenever a project is forked, it copies the settings of the jobs that relate +to it. This means that if you have shared runners setup for a project and +someone forks that project, the shared runners will also serve jobs of this +project. + +# Attack vectors in runners + +Mentioned briefly earlier, but the following things of runners can be exploited. +We're always looking for contributions that can mitigate these [Security Considerations](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md). diff --git a/doc/ci/runners/project_specific.png b/doc/ci/runners/project_specific.png new file mode 100644 index 0000000000000000000000000000000000000000..f51ea694e78f964656c568e4ab95d2977772bd4b GIT binary patch literal 31408 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